diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 78ce050fd..bb7b4999c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -52,7 +52,8 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - 0.18.15.0 + - 0.18.15.1 / 0.18.15.2 on macOS + - 0.19.8.0 (unstable) - Other validations: required: true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index db7c27941..0bfac9a82 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -6,7 +6,7 @@ using System; namespace Barotrauma { - public class Camera : IDisposable + class Camera : IDisposable { public static bool FollowSub = true; @@ -179,9 +179,9 @@ namespace Barotrauma { if (Character.Controlled != null && !Character.Controlled.IsDead) { return; } - msg.Write((byte)ClientNetObject.SPECTATING_POS); - msg.Write(position.X); - msg.Write(position.Y); + msg.WriteByte((byte)ClientNetObject.SPECTATING_POS); + msg.WriteSingle(position.X); + msg.WriteSingle(position.Y); } private void CreateMatrices() diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index 77339f0b1..ed7ff67d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -159,7 +159,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; } #endif - timer += CoroutineManager.UnscaledDeltaTime; + timer += CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index bfb8b7202..c43d3f26b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -73,6 +73,11 @@ namespace Barotrauma } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 80.0f, State.ToString(), stateColor, Color.Black); + if (State == AIState.Attack && selectedTargetingParams != null && selectedTargetingParams.AttackPattern == AttackPattern.Circle) + { + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 100.0f, CirclePhase.ToString(), stateColor, Color.Black); + } + if (LatchOntoAI != null && (State == AIState.Idle || LatchOntoAI.IsAttachedToSub)) { foreach (Joint attachJoint in LatchOntoAI.AttachJoints) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 5e315ee8f..dc50608c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -1,15 +1,14 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Particles; using Barotrauma.SpriteDeformations; -using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; -using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; using System.Collections.Generic; -using Barotrauma.Particles; +using System.Linq; namespace Barotrauma { @@ -55,21 +54,34 @@ namespace Barotrauma if (character.MemState[0].SelectedItem == null || character.MemState[0].SelectedItem.Removed) { - character.SelectedConstruction = null; + character.SelectedItem = null; } - else + else if (character.SelectedItem != character.MemState[0].SelectedItem) { - if (character.SelectedConstruction != character.MemState[0].SelectedItem) + foreach (var ic in character.MemState[0].SelectedItem.Components) { - foreach (var ic in character.MemState[0].SelectedItem.Components) + if (ic.CanBeSelected) { - if (ic.CanBeSelected) - { - ic.Select(character); - } + ic.Select(character); } } - character.SelectedConstruction = character.MemState[0].SelectedItem; + character.SelectedItem = character.MemState[0].SelectedItem; + } + + if (character.MemState[0].SelectedSecondaryItem == null || character.MemState[0].SelectedSecondaryItem.Removed) + { + character.SelectedSecondaryItem = null; + } + else if (character.SelectedSecondaryItem != character.MemState[0].SelectedSecondaryItem) + { + foreach (var ic in character.MemState[0].SelectedSecondaryItem.Components) + { + if (ic.CanBeSelected) + { + ic.Select(character); + } + } + character.SelectedSecondaryItem = character.MemState[0].SelectedSecondaryItem; } if (character.MemState[0].Animation == AnimController.Animation.CPR) @@ -201,15 +213,24 @@ namespace Barotrauma { if (serverPos.SelectedItem == null || serverPos.SelectedItem.Removed) { - character.SelectedConstruction = null; + character.SelectedItem = null; } - else if (serverPos.SelectedItem != null) + else if (character.SelectedItem != serverPos.SelectedItem) { - if (character.SelectedConstruction != serverPos.SelectedItem) - { - serverPos.SelectedItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); - } - character.SelectedConstruction = serverPos.SelectedItem; + serverPos.SelectedItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); + character.SelectedItem = serverPos.SelectedItem; + } + } + if (localPos.SelectedSecondaryItem != serverPos.SelectedSecondaryItem) + { + if (serverPos.SelectedSecondaryItem == null || serverPos.SelectedSecondaryItem.Removed) + { + character.SelectedSecondaryItem = null; + } + else if (character.SelectedSecondaryItem != serverPos.SelectedSecondaryItem) + { + serverPos.SelectedSecondaryItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); + character.SelectedSecondaryItem = serverPos.SelectedSecondaryItem; } } @@ -496,12 +517,11 @@ namespace Barotrauma float maxDepth = 0.0f; float minDepth = 1.0f; float depthOffset = 0.0f; - var ladder = character.SelectedConstruction?.GetComponent(); - - if (ladder != null) + + if (character.SelectedSecondaryItem?.GetComponent() is Ladder ladder) { CalculateLimbDepths(); - if (character.WorldPosition.X < character.SelectedConstruction.WorldPosition.X) + if (character.WorldPosition.X < character.SelectedSecondaryItem.WorldPosition.X) { //at the left side of the ladder, needs to be drawn in front of the rungs if (maxDepth > ladder.BackgroundSpriteDepth) @@ -522,16 +542,21 @@ namespace Barotrauma else { CalculateLimbDepths(); - var controller = character.SelectedConstruction?.GetComponent(); - if (controller != null && controller.ControlCharacterPose && controller.User == character && controller.UserInCorrectPosition) + AdjustDepthOffset(character.SelectedItem); + AdjustDepthOffset(character.SelectedSecondaryItem); + + void AdjustDepthOffset(Item item) { - if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind) + if (item?.GetComponent() is { ControlCharacterPose: true, UserInCorrectPosition: true } controller && controller.User == character) { - depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); - } - else - { - depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f); + if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind) + { + depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); + } + else + { + depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 3c015ee76..99557594c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -9,7 +9,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -323,8 +322,8 @@ namespace Barotrauma { cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected * item.OffsetOnSelectedMultiplier; } - else if (SelectedConstruction != null && ViewTarget == null && - SelectedConstruction.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this))) + else if (SelectedItem != null && ViewTarget == null && + SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this))) { cam.OffsetAmount = targetOffsetAmount = 0.0f; cursorPosition = @@ -368,21 +367,20 @@ namespace Barotrauma if (!GUI.InputBlockingMenuOpen) { - if (SelectedConstruction != null && - (SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) || + if (SelectedItem != null && + (SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) || ((ViewTarget as Item)?.Prefab.FocusOnSelected ?? false) && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))) { if (GameMain.Client != null) { - //emulate a Select input to get the character to deselect the item server-side - //keys[(int)InputType.Select].Hit = true; - keys[(int)InputType.Deselect].Hit = true; + //emulate a Deselect input to get the character to deselect the item server-side + EmulateInput(InputType.Deselect); } //reset focus to prevent us from accidentally interacting with another entity focusedItem = null; FocusedCharacter = null; findFocusedTimer = 0.2f; - SelectedConstruction = null; + SelectedItem = null; } } @@ -425,6 +423,11 @@ namespace Barotrauma } } } + + public void EmulateInput(InputType input) + { + keys[(int)input].Hit = true; + } partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { @@ -518,7 +521,7 @@ namespace Barotrauma //reduce the amount of aim assist if an item has been selected //= can't switch selection to another item without deselecting the current one first UNLESS the cursor is directly on the item //otherwise it would be too easy to accidentally switch the selected item when rewiring items - float aimAssistAmount = SelectedConstruction == null ? 100.0f * aimAssistModifier : 1.0f; + float aimAssistAmount = SelectedItem == null ? 100.0f * aimAssistModifier : 1.0f; Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition); @@ -623,12 +626,12 @@ namespace Barotrauma { if (this != controlled) { return false; } if (GameMain.GameSession?.Campaign != null && GameMain.GameSession.Campaign.ShowCampaignUI) { return true; } - var controller = SelectedConstruction?.GetComponent(); + var controller = SelectedItem?.GetComponent(); //lock if using a controller, except if we're also using a connection panel in the same item return - SelectedConstruction != null && + SelectedItem != null && controller?.User == this && controller.HideHUD && - SelectedConstruction?.GetComponent()?.User != this; + SelectedItem?.GetComponent()?.User != this; } @@ -900,7 +903,14 @@ namespace Barotrauma if (info != null) { LocalizedString name = Info.DisplayName; - if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } + if (controlled == null && name != Info.Name) + { + name += " " + TextManager.Get("Disguised"); + } + else if (Info.Title != null) + { + name += '\n' + Info.Title; + } Vector2 nameSize = GUIStyle.Font.MeasureString(name); Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 02e09c587..f4742ac64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Tutorials; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -108,7 +109,7 @@ namespace Barotrauma private static bool ShouldDrawInventory(Character character) { - var controller = character.SelectedConstruction?.GetComponent(); + var controller = character.SelectedItem?.GetComponent(); return character?.Inventory != null && @@ -173,8 +174,8 @@ namespace Barotrauma { if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen) { - bool mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && GUI.MouseOn == null; - if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked()) + bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null; + if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None()) { CharacterHealth.OpenHealthWindow = character.CharacterHealth; } @@ -398,32 +399,63 @@ namespace Barotrauma progressBar.Draw(spriteBatch, cam); } - foreach (Character npc in Character.CharacterList) + void DrawInteractionIcon(Entity entity, Identifier iconStyle) { - if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } + if (entity == null || entity.Removed) { return; } - var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); - if (iconStyle == null) { continue; } - Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); - if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) + Hull currentHull = entity switch { - //TODO: we could probably do better than just hardcoding - //a check for InteractionType.Examine here. + Character character => character.CurrentHull, + Item item => item.CurrentHull, + _ => null + }; + Range visibleRange = new Range(currentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); + LocalizedString label = null; + if (entity is Character characterEntity) + { + if (characterEntity.IsDead || characterEntity.IsIncapacitated) { return; } + if (characterEntity?.CampaignInteractionType == CampaignMode.InteractionType.Examine) + { + //TODO: we could probably do better than just hardcoding + //a check for InteractionType.Examine here. - if (Vector2.DistanceSquared(character.Position, npc.Position) > 500f * 500f) { continue; } + if (Vector2.DistanceSquared(character.Position, entity.Position) > 500f * 500f) { return; } - var body = Submarine.CheckVisibility(character.SimPosition, npc.SimPosition, ignoreLevel: true); - if (body != null && body.UserData as Character != npc) { continue; } + var body = Submarine.CheckVisibility(character.SimPosition, entity.SimPosition, ignoreLevel: true); + if (body != null && body.UserData != entity) { return; } - visibleRange = new Range(-100f, 500f); + visibleRange = new Range(-100f, 500f); + } + label = characterEntity?.Info?.Title; } + + if (GUIStyle.GetComponentStyle(iconStyle) is not GUIComponentStyle style) { return; } + + float dist = Vector2.Distance(character.WorldPosition, entity.WorldPosition); + float distFactor = 1.0f - MathUtils.InverseLerp(1000.0f, 3000.0f, dist); + float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor); GUI.DrawIndicator( spriteBatch, - npc.WorldPosition, + entity.WorldPosition, cam, visibleRange, - iconStyle.GetDefaultSprite(), - iconStyle.Color); + style.GetDefaultSprite(), + style.Color * alpha, + label: label); + } + + foreach (Character npc in Character.CharacterList) + { + if (npc.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } + DrawInteractionIcon(npc, ("CampaignInteractionIcon." + npc.CampaignInteractionType).ToIdentifier()); + } + + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial is not null) + { + foreach (var (entity, iconStyle) in tutorialMode.Tutorial.Icons) + { + DrawInteractionIcon(entity, iconStyle); + } } foreach (Item item in Item.ItemList) @@ -436,10 +468,10 @@ namespace Barotrauma } } - if (character.SelectedConstruction != null && - (character.CanInteractWith(character.SelectedConstruction) || Screen.Selected == GameMain.SubEditorScreen)) + if (character.SelectedItem != null && + (character.CanInteractWith(character.SelectedItem) || Screen.Selected == GameMain.SubEditorScreen)) { - character.SelectedConstruction.DrawHUD(spriteBatch, cam, character); + character.SelectedItem.DrawHUD(spriteBatch, cam, character); } if (character.Inventory != null) { @@ -487,7 +519,7 @@ namespace Barotrauma character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, yOffset), targetWidth: HUDLayoutSettings.PortraitArea.Width, true, character.Info.IsDisguisedAsAnother); character.Info.DrawForeground(spriteBatch); } - mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); + mouseOnPortrait = MouseOnCharacterPortrait() && !character.ShouldLockHud(); if (mouseOnPortrait) { GUIStyle.UIGlow.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea, GUIStyle.Green * 0.5f); @@ -534,6 +566,13 @@ namespace Barotrauma } } + public static bool MouseOnCharacterPortrait() + { + if (Character.Controlled == null) { return false; } + if (CharacterHealth.OpenHealthWindow != null || Character.Controlled.SelectedCharacter != null) { return false; } + return HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition); + } + private static void DrawCharacterHoverTexts(SpriteBatch spriteBatch, Camera cam, Character character) { var allItems = character.Inventory?.AllItems; @@ -561,9 +600,15 @@ namespace Barotrauma Color nameColor = character.FocusedCharacter.GetNameColor(); GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); - textPos.X += 10.0f * GUI.Scale; textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; + if (character.FocusedCharacter.Info?.Title != null && !character.FocusedCharacter.Info.Title.IsNullOrEmpty()) + { + GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.Info.Title, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); + textPos.Y += GUIStyle.SubHeadingFont.MeasureString(character.FocusedCharacter.Info.Title.Value).Y; + } + textPos.X += 10.0f * GUI.Scale; + if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", InputType.Use), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 305031f47..ab2942b42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -102,7 +102,7 @@ namespace Barotrauma 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(" ".ToIdentifier(), "".ToIdentifier()))), + TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), PersonalityTrait.DisplayName), font: font) { Padding = Vector4.Zero @@ -521,29 +521,28 @@ namespace Barotrauma Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); - string ragdollFile = inc.ReadString(); - string jobIdentifier = inc.ReadString(); + string ragdollFile = inc.ReadString(); + Identifier npcId = inc.ReadIdentifier(); + uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); - if (!string.IsNullOrEmpty(jobIdentifier)) - { - jobPrefab = JobPrefab.Get(jobIdentifier); - byte skillCount = inc.ReadByte(); - for (int i = 0; i < skillCount; i++) + if (jobIdentifier > 0) + { + jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); + foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) { - Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); - skillLevels.Add(skillIdentifier, skillLevel); - } - } + skillLevels.Add(skillPrefab.Identifier, skillLevel); + } + } // TODO: animations - CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant) + CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { - ID = infoID, + ID = infoID }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; @@ -777,7 +776,21 @@ namespace Barotrauma createColorSelector($"Customization.{nameof(info.Head.SkinColor)}".ToIdentifier(), info.SkinColors, () => info.Head.SkinColor, (color) => info.Head.SkinColor = color); - +#if DEBUG + new GUIButton(new RectTransform(Vector2.One * 0.12f, + parentComponent.RectTransform, + anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) + { RelativeOffset = new Vector2(0.01f, 0.005f) }, style: "SaveButton", color: Color.Magenta) + { + ToolTip = "DEBUG ONLY: copy the character info XML to clipboard", + OnClicked = (button, o) => + { + XElement element = info.Save(null); + Clipboard.SetText(element.ToString()); + return false; + } + }; +#endif RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, parentComponent.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 49a1eb6d8..4a9a99547 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,8 +1,8 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -44,7 +44,8 @@ namespace Barotrauma LastNetworkUpdateID, AnimController.TargetDir, SelectedCharacter, - SelectedConstruction, + SelectedItem, + SelectedSecondaryItem, AnimController.Anim); memLocalState.Add(posInfo); @@ -114,34 +115,34 @@ namespace Barotrauma public void ClientWriteInput(IWriteMessage msg) { - msg.Write((byte)ClientNetObject.CHARACTER_INPUT); + msg.WriteByte((byte)ClientNetObject.CHARACTER_INPUT); if (memInput.Count > 60) { memInput.RemoveRange(60, memInput.Count - 60); } - msg.Write(LastNetworkUpdateID); + msg.WriteUInt16(LastNetworkUpdateID); byte inputCount = Math.Min((byte)memInput.Count, (byte)60); - msg.Write(inputCount); + msg.WriteByte(inputCount); for (int i = 0; i < inputCount; i++) { msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); - msg.Write(memInput[i].intAim); + msg.WriteUInt16(memInput[i].intAim); if (memInput[i].states.HasFlag(InputNetFlags.Select) || memInput[i].states.HasFlag(InputNetFlags.Deselect) || memInput[i].states.HasFlag(InputNetFlags.Use) || memInput[i].states.HasFlag(InputNetFlags.Health) || memInput[i].states.HasFlag(InputNetFlags.Grab)) { - msg.Write(memInput[i].interact); + msg.WriteUInt16(memInput[i].interact); } } } public virtual void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}"); } + if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}"); } msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (eventData) @@ -150,16 +151,16 @@ namespace Barotrauma Inventory.ClientEventWrite(msg, inventoryStateEventData); break; case TreatmentEventData _: - msg.Write(AnimController.Anim == AnimController.Animation.CPR); + msg.WriteBoolean(AnimController.Anim == AnimController.Animation.CPR); break; case CharacterStatusEventData _: //do nothing break; case UpdateTalentsEventData _: - msg.Write((ushort)characterTalents.Count); + msg.WriteUInt16((ushort)characterTalents.Count); foreach (var unlockedTalent in characterTalents) { - msg.Write(unlockedTalent.Prefab.UintIdentifier); + msg.WriteUInt32(unlockedTalent.Prefab.UintIdentifier); } break; default: @@ -175,6 +176,11 @@ namespace Barotrauma AnimController.Frozen = false; Enabled = true; + //if we start receiving position updates, it means the character's no longer disabled + if (DisabledByEvent && !Removed) + { + DisabledByEvent = false; + } UInt16 networkUpdateID = 0; if (msg.ReadBoolean()) @@ -219,15 +225,17 @@ namespace Barotrauma bool entitySelected = msg.ReadBoolean(); Character selectedCharacter = null; - Item selectedItem = null; + Item selectedItem = null, selectedSecondaryItem = null; AnimController.Animation animation = AnimController.Animation.None; if (entitySelected) { ushort characterID = msg.ReadUInt16(); ushort itemID = msg.ReadUInt16(); + ushort secondaryItemID = msg.ReadUInt16(); selectedCharacter = FindEntityByID(characterID) as Character; selectedItem = FindEntityByID(itemID) as Item; + selectedSecondaryItem = FindEntityByID(secondaryItemID) as Item; if (characterID != NullEntityID) { bool doingCpr = msg.ReadBoolean(); @@ -274,7 +282,7 @@ namespace Barotrauma pos, rotation, networkUpdateID, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); + selectedCharacter, selectedItem, selectedSecondaryItem, animation); while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) index++; @@ -286,7 +294,7 @@ namespace Barotrauma pos, rotation, linearVelocity, angularVelocity, sendingTime, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); + selectedCharacter, selectedItem, selectedSecondaryItem, animation); while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) index++; @@ -375,9 +383,15 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + //it's possible to get these errors when mid-round syncing, as the client may not + //yet know about afflictions that have given the character extra limbs (e.g. spineling genes) + //ignoring the error should be safe though, not executing the attack should not cause any further issues + if (!GameMain.Client.MidRoundSyncing) + { + string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -458,25 +472,11 @@ namespace Barotrauma break; case EventType.AddToCrew: GameMain.GameSession.CrewManager.AddCharacter(this); - CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); - ushort itemCount = msg.ReadUInt16(); - for (int i = 0; i < itemCount; i++) - { - ushort itemID = msg.ReadUInt16(); - if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } - item.AllowStealing = true; - var wifiComponent = item.GetComponent(); - if (wifiComponent != null) - { - wifiComponent.TeamID = teamID; - } - var idCard = item.GetComponent(); - if (idCard != null) - { - idCard.TeamID = teamID; - idCard.SubmarineSpecificID = 0; - } - } + ReadItemTeamChange(msg, true); + break; + case EventType.RemoveFromCrew: + GameMain.GameSession.CrewManager.RemoveCharacter(this, removeInfo: true); + ReadItemTeamChange(msg, false); break; case EventType.UpdateExperience: int experienceAmount = msg.ReadInt32(); @@ -507,9 +507,27 @@ namespace Barotrauma info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); } break; - } msg.ReadPadBits(); + + static void ReadItemTeamChange(IReadMessage msg, bool allowStealing) + { + var itemTeamChange = INetSerializableStruct.Read(msg); + foreach (var itemID in itemTeamChange.ItemIds) + { + if (FindEntityByID(itemID) is not Item item) { continue; } + item.AllowStealing = allowStealing; + if (item.GetComponent() is { } wifiComponent) + { + wifiComponent.TeamID = itemTeamChange.TeamId; + } + if (item.GetComponent() is { } idCard) + { + idCard.TeamID = itemTeamChange.TeamId; + idCard.SubmarineSpecificID = 0; + } + } + } } public static Character ReadSpawnData(IReadMessage inc) @@ -526,6 +544,7 @@ namespace Barotrauma Vector2 position = new Vector2(inc.ReadSingle(), inc.ReadSingle()); bool enabled = inc.ReadBoolean(); + bool disabledByEvent = inc.ReadBoolean(); DebugConsole.Log("Received spawn data for " + speciesName); @@ -561,7 +580,7 @@ namespace Barotrauma CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); try { - character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); + character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.SessionId != ownerId, hasAi: hasAi); } catch (Exception e) { @@ -642,7 +661,7 @@ namespace Barotrauma GameMain.GameSession.CrewManager.AddCharacter(character); } - if (GameMain.Client.ID == ownerId) + if (GameMain.Client.SessionId == ownerId) { GameMain.Client.HasSpawned = true; GameMain.Client.Character = character; @@ -659,7 +678,14 @@ namespace Barotrauma } } - character.Enabled = Controlled == character || enabled; + if (disabledByEvent) + { + character.DisabledByEvent = true; + } + else + { + character.Enabled = Controlled == character || enabled; + } return character; } @@ -673,16 +699,17 @@ namespace Barotrauma AfflictionPrefab causeOfDeathAffliction = null; if (causeOfDeathType == CauseOfDeathType.Affliction) { - string afflictionName = msg.ReadString(); - if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionName)) + uint afflictionId = msg.ReadUInt32(); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionId); + if (afflictionPrefab == null) { - string errorMsg = $"Error in CharacterNetworking.ReadStatus: affliction not found ({afflictionName})"; + string errorMsg = $"Error in CharacterNetworking.ReadStatus: affliction not found (id {afflictionId})"; causeOfDeathType = CauseOfDeathType.Unknown; - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:AfflictionIndexOutOfBounts", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:AfflictionNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else { - causeOfDeathAffliction = AfflictionPrefab.Prefabs[afflictionName]; + causeOfDeathAffliction = afflictionPrefab; } } bool containsAfflictionData = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 48e87f489..983906c72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -162,11 +162,7 @@ namespace Barotrauma openHealthWindow.characterName.Text = value.Character.Info.DisplayName; value.Character.Info.CheckDisguiseStatus(false); } - - if (Character.Controlled.SelectedConstruction != null && Character.Controlled.SelectedConstruction.GetComponent() == null) - { - Character.Controlled.SelectedConstruction = null; - } + Character.Controlled.SelectedItem = null; } HintManager.OnShowHealthInterface(); @@ -284,6 +280,7 @@ namespace Barotrauma cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") { + UserData = UIHighlightAction.ElementId.CPRButton, OnClicked = (button, userData) => { Character selectedCharacter = Character.Controlled?.SelectedCharacter; @@ -694,7 +691,7 @@ namespace Barotrauma { distortTimer = (distortTimer + deltaTime * distortSpeed) % MathHelper.TwoPi; Character.BlurStrength = (float)(Math.Sin(distortTimer) + 1.5f) * 0.25f * blurStrength; - Character.DistortStrength = (float)(Math.Sin(distortTimer) + 1.0f) * 0.1f * distortStrength; + Character.DistortStrength = (float)(Math.Sin(distortTimer) + 1.0f) * 0.05f * distortStrength; } else { @@ -724,7 +721,7 @@ namespace Barotrauma //emulate a Health input to get the character to deselect the item server-side if (GameMain.Client != null) { - Character.Controlled.Keys[(int)InputType.Health].Hit = true; + Character.Controlled.EmulateInput(InputType.Health); } OpenHealthWindow = null; } @@ -1298,6 +1295,7 @@ namespace Barotrauma Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, normalize: true, + user: Character.Controlled, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); @@ -1341,13 +1339,13 @@ namespace Barotrauma }; var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader") - { + { UserData = item, DisabledColor = Color.White * 0.1f, PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { - if (!(userdata is ItemPrefab itemPrefab)) { return false; } + if (userdata is not ItemPrefab itemPrefab) { return false; } var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); @@ -1465,7 +1463,7 @@ namespace Barotrauma description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); - int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); + int vitalityDecrease = (int)GetVitalityDecreaseWithVitalityMultipliers(affliction); if (vitalityDecrease == 0) { vitality.Visible = false; @@ -1507,7 +1505,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { - float afflictionVitalityDecrease = affliction.GetVitalityDecrease(this); + float afflictionVitalityDecrease = GetVitalityDecreaseWithVitalityMultipliers(affliction); Color afflictionEffectColor = Color.White; if (afflictionVitalityDecrease > 0.0f) { @@ -1590,7 +1588,7 @@ namespace Barotrauma affliction.Strength / affliction.Prefab.MaxStrength); var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; - int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); + int vitalityDecrease = (int)GetVitalityDecreaseWithVitalityMultipliers(affliction); if (vitalityDecrease == 0) { vitalityText.Visible = false; @@ -1608,7 +1606,7 @@ namespace Barotrauma { //items can be dropped outside the health window if (!ignoreMousePos && - !healthWindow.Rect.Contains(PlayerInput.MousePosition) ) + !healthWindow.Rect.Contains(PlayerInput.MousePosition)) { return false; } @@ -1624,10 +1622,10 @@ namespace Barotrauma } } - Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); - + Limb targetLimb = + Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex) ?? + Character.AnimController.MainLimb; item.ApplyTreatment(Character.Controlled, Character, targetLimb); - return true; } private void UpdateLimbIndicators(float deltaTime, Rectangle drawArea) @@ -1690,7 +1688,7 @@ namespace Barotrauma if (!affliction.ShouldShowIcon(Character)) { continue; } if (!affliction.Prefab.IsBuff) { - negativeEffect += affliction.Strength; + negativeEffect += affliction.Strength * GetVitalityMultiplier(affliction, limbHealth); } else { @@ -1904,6 +1902,7 @@ namespace Barotrauma public void ClientRead(IReadMessage inc) { newAfflictions.Clear(); + newPeriodicEffects.Clear(); byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -1925,7 +1924,7 @@ namespace Barotrauma int periodicAfflictionCount = inc.ReadByte(); for (int j = 0; j < periodicAfflictionCount; j++) { - float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); + float periodicAfflictionTimer = inc.ReadRangedSingle(0, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } newAfflictions.Add((null, afflictionPrefab, afflictionStrength)); @@ -1995,13 +1994,13 @@ namespace Barotrauma //timer has wrapped around, apply the effect if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; } } @@ -2014,7 +2013,7 @@ namespace Barotrauma FaceTint = DefaultFaceTint; BodyTint = Color.TransparentBlack; - if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } + if (!Character.Params.Health.ApplyAfflictionColors) { return; } foreach (KeyValuePair kvp in afflictions) { @@ -2031,15 +2030,21 @@ namespace Barotrauma foreach (Limb limb in Character.AnimController.Limbs) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } - limb.BurnOverlayStrength = 0.0f; limb.DamageOverlayStrength = 0.0f; foreach (KeyValuePair kvp in afflictions) { - if (kvp.Value != limbHealths[limb.HealthIndex]) { continue; } var affliction = kvp.Key; - limb.BurnOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; - limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; + float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; + if (kvp.Value == limbHealths[limb.HealthIndex]) + { + limb.BurnOverlayStrength += burnStrength; + limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; + } + else + { + limb.BurnOverlayStrength += burnStrength / 2; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 7fbcef8fe..4a8c36df7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -481,7 +481,7 @@ namespace Barotrauma { ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) - ?? ContentPath.FromRaw(character.Prefab.ContentPackage, spriteParams.GetTexturePath()); + ?? ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); path = GetSpritePath(texturePath); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 088fb2725..85877787b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -63,7 +63,7 @@ namespace Barotrauma files = contentPackage.Files.Select(File.FromContentFile).ToList(); ModVersion = IncrementModVersion(contentPackage.ModVersion); IsCore = contentPackage is CorePackage; - SteamWorkshopId = contentPackage.SteamWorkshopId; + UgcId = contentPackage.UgcId; ExpectedHash = contentPackage.Hash; InstallTime = contentPackage.InstallTime; } @@ -74,7 +74,7 @@ namespace Barotrauma get => name; set { - var charsToRemove = Path.GetInvalidFileNameChars(); + var charsToRemove = Path.GetInvalidFileNameCharsCrossPlatform(); name = string.Concat(value.Where(c => !charsToRemove.Contains(c))); } } @@ -90,9 +90,9 @@ namespace Barotrauma public bool IsCore = false; - public UInt64 SteamWorkshopId = 0; + public Option UgcId = Option.None(); - public DateTime? InstallTime = null; + public Option InstallTime = Option.None(); public bool HasFile(File file) => Files.Any(f => @@ -120,7 +120,7 @@ namespace Barotrauma public void DiscardHashAndInstallTime() { ExpectedHash = null; - InstallTime = null; + InstallTime = Option.None(); } public static string IncrementModVersion(string modVersion) @@ -155,11 +155,11 @@ namespace Barotrauma addRootAttribute("name", Name); if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } addRootAttribute("corepackage", IsCore); - if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } + if (UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId steamWorkshopId) { addRootAttribute("steamworkshopid", steamWorkshopId.Value); } addRootAttribute("gameversion", GameMain.Version); if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } - if (InstallTime != null) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)); } + if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); } files.ForEach(f => rootElement.Add(f.ToXElement())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 86a971bf5..e59354ecb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -45,9 +45,11 @@ namespace Barotrauma var needInstalling = subscribedItems.Where(item => !WorkshopPackages.Any(p - => item.Id == p.SteamWorkshopId - && p.InstallTime.HasValue - && item.LatestUpdateTime <= p.InstallTime)) + => p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && item.Id == workshopId.Value + && p.InstallTime.TryUnwrap(out var installTime) + && item.LatestUpdateTime <= installTime)) .ToArray(); if (needInstalling.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs similarity index 97% rename from Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs rename to Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs index 748995b91..2b766254c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Transition /// Class dedicated to transitioning away from the old, shitty /// Mods + Submarines folders to the new LocalMods folder /// - public static class UgcTransition + public static class LegacySteamUgcTransition { private const string readmeName = "LOCALMODS_README.txt"; @@ -168,7 +168,11 @@ namespace Barotrauma.Transition addHeader(TextManager.Get("SubscribedMods")); foreach (var mod in mods.Mods) { - addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id)); + addTickbox(mod.Dir, mod.Name, + ticked: !(mod.Item is { } item && ContentPackageManager.LocalPackages.Any(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == item.Id))); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d52053c6e..0b244c837 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1,21 +1,18 @@ -using Barotrauma.Items.Components; +using Barotrauma.ClientSource.Settings; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; +using Barotrauma.Steam; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using Barotrauma.IO; +using System.Globalization; using System.Linq; using System.Text; using System.Xml.Linq; -using System.Globalization; -using FarseerPhysics; -using Barotrauma.Extensions; -using Barotrauma.Steam; -using System.Threading.Tasks; -using Barotrauma.ClientSource.Settings; -using Barotrauma.MapCreatures.Behavior; using static Barotrauma.FabricationRecipe; namespace Barotrauma @@ -229,7 +226,7 @@ namespace Barotrauma return client.HasPermission(ClientPermissions.Kick); case "ban": case "banip": - case "banendpoint": + case "banaddress": return client.HasPermission(ClientPermissions.Ban); case "unban": case "unbanip": @@ -432,24 +429,6 @@ namespace Barotrauma } })); - commands.Add(new Command("startlidgrenclient", "", (string[] args) => - { - if (args.Length == 0) return; - - if (GameMain.Client == null) - { - GameMain.Client = new GameClient("Name", args[0], 0); - } - })); - - commands.Add(new Command("startsteamp2pclient", "", (string[] args) => - { - if (GameMain.Client == null) - { - GameMain.Client = new GameClient("Name", null, 76561198977850505); //this is juan's alt account, feel free to abuse this one - } - })); - commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => { CheatsEnabled = true; @@ -744,7 +723,7 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { - Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + Vector2 explosionPos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); @@ -1299,7 +1278,7 @@ namespace Barotrauma int? fabricationCost = null; int? deconstructProductCost = null; - var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab); + var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab && f.RequiredItems.Any()); if (fabricationRecipe != null) { foreach (var ingredient in fabricationRecipe.RequiredItems) @@ -1334,6 +1313,21 @@ namespace Barotrauma if (fabricationRecipe != null) { var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefabs.Contains(targetItem)); + + if (ingredient == null) + { + foreach (var requiredItem in fabricationRecipe.RequiredItems) + { + foreach (var itemPrefab2 in requiredItem.ItemPrefabs) + { + foreach (var recipe in itemPrefab2.FabricationRecipes.Values) + { + ingredient ??= recipe.RequiredItems.Find(r => r.ItemPrefabs.Contains(targetItem)); + } + } + } + } + if (ingredient == null) { NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); @@ -2077,7 +2071,17 @@ namespace Barotrauma var prefab = MapEntityPrefab.Find(null, args[0]); if (prefab != null) { - DebugConsole.NewMessage(prefab.Name + " " + prefab.Identifier + " " + prefab.GetType().ToString()); + NewMessage(prefab.Name + " " + prefab.Identifier + " " + prefab.GetType().ToString()); + } + })); + + commands.Add(new Command("copycharacterinfotoclipboard", "", (string[] args) => + { + if (Character.Controlled?.Info != null) + { + XElement element = Character.Controlled?.Info.Save(null); + Clipboard.SetText(element.ToString()); + DebugConsole.NewMessage($"Copied the characterinfo of {Character.Controlled.Name} to clipboard."); } })); @@ -2485,29 +2489,11 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); + #if DEBUG - commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) => - { - VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null); - })); - - commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => - { - TaskPool.Add("DebugQueryLobbies", - SteamManager.LobbyQueryRequest(), (t) => - { - t.TryGetResult(out List lobbies); - foreach (var lobby in lobbies) - { - NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow); - } - NewMessage($"Retrieved a total of {lobbies.Count} lobbies", Color.Lime); - }); - })); - commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { - if (args.Length != 1) return; + if (args.Length != 1) { return; } TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier()); })); @@ -2517,9 +2503,20 @@ namespace Barotrauma NPCConversation.WriteToCSV(); })); - commands.Add(new Command("csvtoxml", "csvtoxml [language] -> Converts .csv localization files in Content/NPCConversations & Content/Texts to .xml for use in-game.", (string[] args) => + commands.Add(new Command("csvtoxml", "csvtoxml -> Converts .csv localization files Content/Texts/Texts.csv and Content/Texts/NPCConversations.csv to .xml for use in-game.", (string[] args) => { - LocalizationCSVtoXML.Convert(); + ShowQuestionPrompt("Do you want to save the text files to the project folder (../../../BarotraumaShared/Content/Texts/)? If not, they are saved in the current working directory. Y/N", + (option1) => + { + ShowQuestionPrompt("Do you want to convert the NPC conversations as well? Y/N", + (option2) => + { + LocalizationCSVtoXML.ConvertMasterLocalizationKit( + option1.ToLowerInvariant() == "y" ? "../../../BarotraumaShared/Content/Texts/" : "Content/Texts", + option1.ToLowerInvariant() == "y" ? "../../../BarotraumaShared/Content/NPCConversations/" : "Content/NPCConversations", + convertConversations: option2.ToLowerInvariant() == "y"); + }); + }); })); commands.Add(new Command("printproperties", "Goes through the currently collected property list for missing localizations and writes them to a file.", (string[] args) => @@ -2816,7 +2813,7 @@ namespace Barotrauma ); AssignOnClientExecute( - "banendpoint|banip", + "banaddress|banip", (string[] args) => { if (GameMain.Client == null || args.Length == 0) return; @@ -2838,7 +2835,7 @@ namespace Barotrauma } GameMain.Client?.SendConsoleCommand( - "banendpoint " + + "banaddress " + args[0] + " " + (banDuration.HasValue ? banDuration.Value.TotalSeconds.ToString() : "0") + " " + reason); @@ -2851,13 +2848,16 @@ namespace Barotrauma { if (GameMain.Client == null || args.Length == 0) return; string clientName = string.Join(" ", args); - GameMain.Client.UnbanPlayer(clientName, ""); + GameMain.Client.UnbanPlayer(clientName); })); - commands.Add(new Command("unbanip", "unbanip [ip]: Unban a specific IP.", (string[] args) => + commands.Add(new Command("unbanaddress", "unbanaddress [endpoint]: Unban a specific endpoint.", (string[] args) => { if (GameMain.Client == null || args.Length == 0) return; - GameMain.Client.UnbanPlayer("", args[0]); + if (Endpoint.Parse(args[0]).TryUnwrap(out var endpoint)) + { + GameMain.Client.UnbanPlayer(endpoint); + } })); AssignOnClientExecute( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index d9dd3a9cc..865634dd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -32,12 +32,12 @@ namespace Barotrauma private static bool shouldFadeToBlack; - private bool IsBlockedByAnotherConversation(IEnumerable _) + private bool IsBlockedByAnotherConversation(IEnumerable _, float duration) { return lastActiveAction != null && lastActiveAction.ParentEvent != ParentEvent && - Timing.TotalTime < lastActiveAction.lastActiveTime + BlockOtherConversationsDuration; + Timing.TotalTime < lastActiveAction.lastActiveTime + duration; } partial void ShowDialog(Character speaker, Character targetCharacter) @@ -63,33 +63,45 @@ namespace Barotrauma shouldFadeToBlack = fadeToBlack; + Sprite eventSprite = EventSet.GetEventSprite(spriteIdentifier); + if (lastMessageBox != null && !lastMessageBox.Closed && GUIMessageBox.MessageBoxes.Contains(lastMessageBox)) { - if (actionId != null && lastMessageBox.UserData is Pair userData) + if (eventSprite != null && lastMessageBox.BackgroundIcon == null) { - if (userData.Second == actionId) { return; } - lastMessageBox.UserData = new Pair("ConversationAction", actionId.Value); + //no background icon in the last message box: we need to create a new one + lastMessageBox.Close(); } - - GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; - Debug.Assert(conversationList != null); - - // gray out the last text block - if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) + else { - if (lastElement.FindChild("text", true) is GUITextBlock textLayout) + if (actionId != null && lastMessageBox.UserData is Pair userData) { - textLayout.OverrideTextColor(Color.DarkGray * 0.8f); + if (userData.Second == actionId) { return; } + lastMessageBox.UserData = new Pair("ConversationAction", actionId.Value); } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + + // gray out the last text block + if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) + { + if (lastElement.FindChild("text", true) is GUITextBlock textLayout) + { + textLayout.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + + float prevSize = conversationList.TotalSize; + + List extraButtons = CreateConversation(conversationList, text, speaker, options, string.IsNullOrWhiteSpace(spriteIdentifier)); + AssignActionsToButtons(extraButtons, lastMessageBox); + RecalculateLastMessage(conversationList, true); + conversationList.BarScroll = (prevSize - conversationList.Content.Rect.Height) / (conversationList.TotalSize - conversationList.Content.Rect.Height); + conversationList.ScrollToEnd(duration: 0.5f); + lastMessageBox.SetBackgroundIcon(eventSprite); + return; } - - List extraButtons = CreateConversation(conversationList, text, speaker, options, string.IsNullOrWhiteSpace(spriteIdentifier)); - AssignActionsToButtons(extraButtons, lastMessageBox); - RecalculateLastMessage(conversationList, true); - - conversationList.ScrollToEnd(0.5f); - lastMessageBox.SetBackgroundIcon(EventSet.GetEventSprite(spriteIdentifier)); - return; } var (relative, min) = GetSizes(dialogType); @@ -100,7 +112,10 @@ namespace Barotrauma { UserData = "ConversationAction" }; - + messageBox.OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (Screen.Selected is not GameScreen) { messageBox.Close(); } + }; lastMessageBox = messageBox; messageBox.InnerFrame.ClearChildren(); @@ -308,7 +323,7 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.Get(text).Fallback(text); + LocalizedString translatedText = TextManager.ParseInputTypes(TextManager.Get(text)).Fallback(text); if (speaker?.Info != null && drawChathead) { @@ -368,18 +383,18 @@ namespace Barotrauma private static void SendResponse(UInt16 actionId, int selectedOption) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); - outmsg.Write(actionId); - outmsg.Write((byte)selectedOption); + outmsg.WriteByte((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); + outmsg.WriteUInt16(actionId); + outmsg.WriteByte((byte)selectedOption); GameMain.Client?.ClientPeer?.Send(outmsg, DeliveryMethod.Reliable); } private static void SendIgnore(UInt16 actionId) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); - outmsg.Write(actionId); - outmsg.Write(byte.MaxValue); + outmsg.WriteByte((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); + outmsg.WriteUInt16(actionId); + outmsg.WriteByte(byte.MaxValue); GameMain.Client?.ClientPeer?.Send(outmsg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/InventoryHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/InventoryHighlightAction.cs new file mode 100644 index 000000000..cdcede52f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/InventoryHighlightAction.cs @@ -0,0 +1,58 @@ +using Microsoft.Xna.Framework; +using System.Linq; + +namespace Barotrauma; + +partial class InventoryHighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.Orange; + + partial void UpdateProjSpecific() + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + SetHighlight(target); + } + } + + private void SetHighlight(Entity entity) + { + if (entity is Item item) + { + int i = 0; + foreach (var itemContainer in item.GetComponents()) + { + if (ItemContainerIndex == -1 || i == ItemContainerIndex) + { + SetHighlight(itemContainer.Inventory); + } + i++; + } + } + else if (entity is Character c) + { + SetHighlight(c.Inventory); + } + } + + private void SetHighlight(Inventory inventory) + { + if (inventory?.visualSlots == null) { return; } + for (int i = 0; i < inventory.visualSlots.Length; i++) + { + if (inventory.visualSlots[i].HighlightTimer > 0) { continue; } + Item item = inventory.GetItemAt(i); + if (IsSuitableItem(item) || + (Recursive && item?.OwnInventory != null && item.OwnInventory.FindAllItems(it => IsSuitableItem(it), recursive: true).Any())) + { + inventory.visualSlots[i].ShowBorderHighlight(highlightColor, 0.5f, 0.5f, 0.1f); + } + } + } + + private bool IsSuitableItem(Item item) + { + return (ItemIdentifier.IsEmpty && item == null) || + (item != null && (item.Prefab.Identifier == ItemIdentifier || item.HasTag(ItemIdentifier))); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs new file mode 100644 index 000000000..b75a1af4a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -0,0 +1,77 @@ +using Barotrauma.Tutorials; +using System; +using System.Linq; + +namespace Barotrauma; + +partial class MessageBoxAction : EventAction +{ + partial void UpdateProjSpecific() + { + if (Type == ActionType.Create || Type == ActionType.ConnectObjective) + { + CreateMessageBox(); + if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + Identifier id = Identifier.IfEmpty(Text); + var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); + tutorialMode.Tutorial?.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + } + } + else if (Type == ActionType.Close) + { + GUIMessageBox.Close(Tag); + } + else if (Type == ActionType.Clear) + { + GUIMessageBox.CloseAll(); + } + } + + public void CreateMessageBox() + { + new GUIMessageBox( + headerText: TextManager.Get(Header), + text: RichString.Rich(TextManager.ParseInputTypes(TextManager.Get(Text).Fallback(Text.ToString()), useColorHighlight: true)), + buttons: Array.Empty(), + type: GUIMessageBox.Type.Tutorial, + tag: Tag, + iconStyle: IconStyle, + autoCloseCondition: GetAutoCloseCondition(), + hideCloseButton: HideCloseButton) + { + FlashOnAutoCloseCondition = true + }; + } + + private Func GetAutoCloseCondition() + { + var character = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Character; + Func autoCloseCondition = null; + if (!string.IsNullOrEmpty(CloseOnInput) && Enum.TryParse(CloseOnInput, true, out InputType closeOnInput)) + { + autoCloseCondition = () => PlayerInput.KeyDown(closeOnInput); + } + else if (!CloseOnSelectTag.IsEmpty) + { + autoCloseCondition = () => character?.SelectedItem != null && character.SelectedItem.HasTag(CloseOnSelectTag); + } + else if (!CloseOnPickUpTag.IsEmpty) + { + autoCloseCondition = () => character?.Inventory != null && character.Inventory.FindItemByTag(CloseOnPickUpTag, recursive: true) != null; + } + else if (!CloseOnEquipTag.IsEmpty) + { + autoCloseCondition = () => character != null && character.HasEquippedItem(CloseOnEquipTag); + } + else if (!CloseOnExitRoomName.IsEmpty) + { + autoCloseCondition = () => character?.CurrentHull == null || character.CurrentHull.RoomName.ToIdentifier() != CloseOnExitRoomName; + } + else if (!CloseOnInRoomName.IsEmpty) + { + autoCloseCondition = () => character?.CurrentHull != null && character.CurrentHull.RoomName.ToIdentifier() == CloseOnInRoomName; + } + return autoCloseCondition; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs new file mode 100644 index 000000000..732c1a480 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs @@ -0,0 +1,51 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +partial class TutorialHighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.Orange; + + partial void UpdateProjSpecific() + { + if (GameMain.GameSession?.GameMode is not TutorialMode) { return; } + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + SetHighlight(target); + } + } + + private void SetHighlight(Entity entity) + { + if (entity is Item i) + { + SetItemHighlight(i); + } + else if (entity is Structure s) + { + SetStructureHighlight(s); + } + else if (entity is Character c) + { + SetCharacterHighlight(c); + } + } + + private void SetItemHighlight(Item item) + { + if (item.ExternalHighlight == State) { return; } + item.HighlightColor = State ? highlightColor : null; + item.ExternalHighlight = State; + } + + private void SetStructureHighlight(Structure structure) + { + structure.SpriteColor = State ? highlightColor : Color.White; + structure.ExternalHighlight = State; + } + + private void SetCharacterHighlight(Character character) + { + character.ExternalHighlight = State; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs new file mode 100644 index 000000000..ab9b097d3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs @@ -0,0 +1,50 @@ +using Barotrauma.Tutorials; + +namespace Barotrauma; + +partial class TutorialSegmentAction : EventAction +{ + private Tutorial.Segment segment; + + partial void UpdateProjSpecific() + { + // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) + if (Type == SegmentActionType.Trigger) + { + segment = Tutorial.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), + new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); + } + else if (Type == SegmentActionType.Add) + { + segment = Tutorial.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + if (tutorialMode.Tutorial is Tutorial tutorial) + { + switch (Type) + { + case SegmentActionType.Trigger: + case SegmentActionType.Add: + tutorial.TriggerTutorialSegment(segment); + break; + case SegmentActionType.Complete: + tutorial.CompleteTutorialSegment(Identifier); + break; + case SegmentActionType.Remove: + tutorial.RemoveTutorialSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + tutorial.CompleteTutorialSegment(Identifier); + tutorial.RemoveTutorialSegment(Identifier); + break; + } + } + } + else + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs new file mode 100644 index 000000000..2826454ca --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs @@ -0,0 +1,46 @@ +using Microsoft.Xna.Framework; +using System.Linq; + +namespace Barotrauma; + +partial class UIHighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.Orange; + + partial void UpdateProjSpecific() + { + bool useCircularFlash = false; + GUIComponent component = null; + + if (Id != ElementId.None) + { + component = GUI.GetAdditions().FirstOrDefault(c => Equals(Id, c.UserData)); + } + else if (!EntityIdentifier.IsEmpty) + { + component = GUI.GetAdditions().FirstOrDefault(c => + c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); + } + else if (!OrderIdentifier.IsEmpty) + { + useCircularFlash = true; + if (!OrderTargetTag.IsEmpty) + { + component = + GUI.GetAdditions().FirstOrDefault(c => + c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && + order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); + } + component ??= + GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption) ?? + GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier) ?? + GUI.GetAdditions().FirstOrDefault(c => Equals(OrderCategory, c.UserData)); + } + + if (component != null && component.FlashTimer <= 0.0f) + { + component.Flash(highlightColor, useCircularFlash: useCircularFlash); + component.Bounce |= Bounce; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 103e2cd2c..9093450ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -651,11 +651,11 @@ namespace Barotrauma break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); - + string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 3957dff43..291366b9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -1,6 +1,4 @@ -using Barotrauma.Networking; - -namespace Barotrauma +namespace Barotrauma { partial class CombatMission : Mission { @@ -8,7 +6,7 @@ namespace Barotrauma { get { - if (descriptions == null) return ""; + if (descriptions == null) { return ""; } if (GameMain.Client?.Character == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index cff6b76b9..28eafbc6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -80,6 +80,10 @@ namespace Barotrauma LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; + if (!message.IsNullOrEmpty()) + { + message = ModifyMessage(message); + } CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index ae0a54609..b7b05c7f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -6,23 +6,26 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; +using System.Threading; +using Barotrauma.Threading; namespace Barotrauma { public class ScalableFont : IDisposable { - private static List FontList = new List(); + private static readonly List FontList = new List(); private static Library Lib = null; - private readonly object mutex = new object(); + private static readonly object globalMutex = new object(); + + private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(); - private string filename; - private Face face; + private readonly string filename; + private readonly Face face; private uint size; private int baseHeight; - private Dictionary texCoords; - private List textures; - private GraphicsDevice graphicsDevice; + private readonly Dictionary texCoords; + private readonly List textures; + private readonly GraphicsDevice graphicsDevice; private Vector2 currentDynamicAtlasCoords; private int currentDynamicAtlasNextY; @@ -49,7 +52,7 @@ namespace Barotrauma set { size = value; - if (graphicsDevice != null) RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); + if (graphicsDevice != null) { RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); } } } @@ -93,11 +96,15 @@ namespace Barotrauma public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false) { - lock (mutex) + lock (globalMutex) + { + Lib ??= new Library(); + } + + this.filename = filename; + this.face = null; + using (new ReadLock(rwl)) { - if (Lib == null) Lib = new Library(); - this.filename = filename; - this.face = null; foreach (ScalableFont font in FontList) { if (font.filename == filename) @@ -106,19 +113,23 @@ namespace Barotrauma break; } } - this.face ??= new Face(Lib, filename); - this.size = size; - this.textures = new List(); - this.texCoords = new Dictionary(); - this.DynamicLoading = dynamicLoading; - this.IsCJK = isCJK; - this.graphicsDevice = gd; + } - if (gd != null && !dynamicLoading) - { - RenderAtlas(gd); - } + this.face ??= new Face(Lib, filename); + this.size = size; + this.textures = new List(); + this.texCoords = new Dictionary(); + this.DynamicLoading = dynamicLoading; + this.IsCJK = isCJK; + this.graphicsDevice = gd; + if (gd != null && !dynamicLoading) + { + RenderAtlas(gd); + } + + lock (globalMutex) + { FontList.Add(this); } } @@ -162,7 +173,7 @@ namespace Barotrauma Vector2 currentCoords = Vector2.Zero; int nextY = 0; - lock (mutex) + using (new WriteLock(rwl)) { face.SetPixelSizes(0, size); face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal); @@ -175,19 +186,22 @@ namespace Barotrauma for (uint j = start; j <= end; j++) { uint glyphIndex = face.GetCharIndex(j); - if (glyphIndex == 0) continue; + if (glyphIndex == 0) + { + texCoords.Add(j, new GlyphData( + advance: 0, + texIndex: -1)); + continue; + } face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) - { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty + //glyph is empty, but char might still apply advance + GlyphData blankData = new GlyphData( + advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(j, blankData); - } + texCoords.Add(j, blankData); continue; } //stacktrace doesn't really work that well when RenderGlyph throws an exception @@ -257,7 +271,7 @@ namespace Barotrauma private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) { bool missingCharacterFound = false; - lock (mutex) + using (new ReadLock(rwl)) { missingCharacterFound = !texCoords.ContainsKey(character); } @@ -268,10 +282,9 @@ namespace Barotrauma private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54) { bool missingCharacterFound = false; - var distinctChrs = str.Distinct().Select(c => (uint)c).ToArray(); - lock (mutex) + using (new ReadLock(rwl)) { - foreach (var character in distinctChrs) + foreach (var character in str) { if (texCoords.ContainsKey(character)) { continue; } @@ -280,7 +293,7 @@ namespace Barotrauma } } if (!missingCharacterFound) { return; } - DynamicRenderAtlas(gd, distinctChrs, texDims, baseChar); + DynamicRenderAtlas(gd, str.Select(c => (uint)c), texDims, baseChar); } private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable characters, int texDims = 1024, uint baseChar = 0x54) @@ -299,7 +312,7 @@ namespace Barotrauma Fixed26Dot6 horizontalAdvance; Vector2 drawOffset; - lock (mutex) + using (new WriteLock(rwl)) { if (textures.Count == 0) { @@ -318,20 +331,23 @@ namespace Barotrauma if (texCoords.ContainsKey(character)) { continue; } uint glyphIndex = face.GetCharIndex(character); - if (glyphIndex == 0) { continue; } + if (glyphIndex == 0) + { + texCoords.Add(character, new GlyphData( + advance: 0, + texIndex: -1)); + continue; + } face.SetPixelSizes(0, size); face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) - { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(character, blankData); - } + //glyph is empty, but char might still apply advance + GlyphData blankData = new GlyphData( + advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + texIndex: -1); //indicates no texture because the glyph is empty + texCoords.Add(character, blankData); continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 3371cd5b7..2cc5886c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -325,6 +325,12 @@ namespace Barotrauma ToggleOpen = PreferChatBoxOpen = GameSettings.CurrentConfig.ChatOpen; } + public void Toggle() + { + ToggleOpen = !ToggleOpen; + CloseAfterMessageSent = false; + } + public bool TypingChatMessage(GUITextBox textBox, string text) { string command = ChatMessage.GetChatMessageCommand(text, out _); @@ -611,22 +617,29 @@ namespace Barotrauma showNewMessagesButton.Visible = false; } - if (PlayerInput.KeyHit(InputType.ToggleChatMode) && GUI.KeyboardDispatcher.Subscriber == null && Screen.Selected == GameMain.GameScreen) + if (Screen.Selected == GameMain.GameScreen && GUI.KeyboardDispatcher.Subscriber == null) { - try + if (PlayerInput.KeyHit(InputType.ToggleChatMode)) { - var mode = GameMain.ActiveChatMode switch + try { - ChatMode.Local => ChatMode.Radio, - ChatMode.Radio => ChatMode.Local, - _ => throw new NotImplementedException() - }; - ChatModeDropDown.SelectItem(mode); - // TODO: Play a sound? + var mode = GameMain.ActiveChatMode switch + { + ChatMode.Local => ChatMode.Radio, + ChatMode.Radio => ChatMode.Local, + _ => throw new NotImplementedException() + }; + ChatModeDropDown.SelectItem(mode); + // TODO: Play a sound? + } + catch (NotImplementedException) + { + DebugConsole.ThrowError($"Error toggling chat mode: not implemented for current mode \"{GameMain.ActiveChatMode}\""); + } } - catch (NotImplementedException) + else if (PlayerInput.KeyHit(InputType.ChatBox)) { - DebugConsole.ThrowError($"Error toggling chat mode: not implemented for current mode \"{GameMain.ActiveChatMode}\""); + Toggle(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 5760ecefd..b25b07ca4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -128,10 +128,11 @@ namespace Barotrauma var sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), hireablesGroup.RectTransform), isHorizontal: true) { - RelativeSpacing = 0.015f + RelativeSpacing = 0.015f, + Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby")); - sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), sortGroup.RectTransform), elementCount: 5) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby")); + sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), elementCount: 5) { OnSelected = (child, userData) => { @@ -193,19 +194,20 @@ namespace Barotrauma { RelativeSpacing = 0.01f }; - validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) + validateHiresButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; - clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) + clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() }; + GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock); resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -528,7 +530,7 @@ namespace Barotrauma if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get("personalitytrait." + trait.Name.Replace(" ".ToIdentifier(), Identifier.Empty))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), trait.DisplayName); } infoLabelGroup.Recalculate(); infoValueGroup.Recalculate(); @@ -887,35 +889,35 @@ namespace Barotrauma if (campaign is MultiPlayerCampaign) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.CREW); + msg.WriteByte((byte)ClientPacketHeader.CREW); - msg.Write(updatePending); + msg.WriteBoolean(updatePending); if (updatePending) { - msg.Write((ushort)PendingHires.Count); + msg.WriteUInt16((ushort)PendingHires.Count); foreach (CharacterInfo pendingHire in PendingHires) { - msg.Write(pendingHire.GetIdentifierUsingOriginalName()); + msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); } } - msg.Write(validateHires); + msg.WriteBoolean(validateHires); bool validRenaming = renameCharacter.info != null && !string.IsNullOrEmpty(renameCharacter.newName); - msg.Write(validRenaming); + msg.WriteBoolean(validRenaming); if (validRenaming) { int identifier = renameCharacter.info.GetIdentifierUsingOriginalName(); - msg.Write(identifier); - msg.Write(renameCharacter.newName); + msg.WriteInt32(identifier); + msg.WriteString(renameCharacter.newName); bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.GetIdentifierUsingOriginalName() == identifier) ?? false; - msg.Write(existingCrewMember); + msg.WriteBoolean(existingCrewMember); } - msg.Write(firedCharacter != null); + msg.WriteBoolean(firedCharacter != null); if (firedCharacter != null) { - msg.Write(firedCharacter.GetIdentifier()); + msg.WriteInt32(firedCharacter.GetIdentifier()); } GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 49f027c19..c158fc21d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -48,7 +48,7 @@ namespace Barotrauma WaitingBackground = 6, // Cursor + Hourglass } - public static class GUI + static class GUI { public static GUICanvas Canvas => GUICanvas.Instance; public static CursorState MouseCursor = CursorState.Default; @@ -410,7 +410,7 @@ namespace Barotrauma { y += yStep; DrawString(spriteBatch, new Vector2(10, y), - "Sub pos: " + Submarine.MainSub.Position.ToPoint(), + "Sub pos: " + Submarine.MainSub.WorldPosition.ToPoint(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } @@ -879,6 +879,11 @@ namespace Barotrauma } } } + + public static IEnumerable GetAdditions() + { + return additions; + } #endregion public static GUIComponent MouseOn { get; private set; } @@ -958,7 +963,7 @@ namespace Barotrauma // Wire cursors if (Character.Controlled != null) { - if (Character.Controlled.SelectedConstruction?.GetComponent() != null) + if (Character.Controlled.SelectedItem?.GetComponent() != null) { if (Connection.DraggingConnected != null) { @@ -981,7 +986,7 @@ namespace Barotrauma return editor.GetMouseCursorState(); // Portrait area during gameplay case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true): - if (HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) || CharacterHealth.IsMouseOnHealthBar()) + if (CharacterHUD.MouseOnCharacterPortrait() || CharacterHealth.IsMouseOnHealthBar()) { return CursorState.Hand; } @@ -1018,6 +1023,11 @@ namespace Barotrauma if (c.Enabled) { + var dragHandle = c as GUIDragHandle ?? parent as GUIDragHandle; + if (dragHandle != null) + { + return dragHandle.Dragging ? CursorState.Dragging : CursorState.Hand; + } // Some parent elements take priority // but not when the child is a GUIButton or GUITickBox if (!(parent is GUIButton) && !(parent is GUIListBox) || @@ -1027,6 +1037,7 @@ namespace Barotrauma } } + // Children in list boxes can be interacted with despite not having // a GUIButton inside of them so instead of hard coding we check if // the children can be interacted with by checking their hover state @@ -1097,6 +1108,8 @@ namespace Barotrauma return list; case GUIScrollBar bar: return bar; + case GUIDragHandle dragHandle: + return dragHandle; } } component = parent; @@ -1344,7 +1357,7 @@ namespace Barotrauma /// Should the indicator move based on the camera position? /// Override the distance-based alpha value with the specified alpha value public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Range visibleRange, Sprite sprite, in Color color, - bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) + bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null, LocalizedString label = null) { Vector2 diff = worldPosition - cam.WorldViewCenter; float dist = diff.Length(); @@ -1394,10 +1407,6 @@ namespace Barotrauma angle = MathHelper.Lerp(originalAngle, angle, MathHelper.Clamp(((screenDist + 10f) - iconDiff.Length()) / 10f, 0f, 1f)); - /*Vector2 unclampedDiff = new Vector2( - (float)Math.Cos(angle) * screenDist, - (float)-Math.Sin(angle) * screenDist);*/ - iconDiff = new Vector2( (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist), (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist)); @@ -1405,7 +1414,20 @@ namespace Barotrauma Vector2 iconPos = cam.WorldToScreen(cam.WorldViewCenter) + iconDiff; sprite.Draw(spriteBatch, iconPos, color * alpha, rotate: 0.0f, scale: symbolScale); - if (/*unclampedDiff.Length()*/ screenDist - 10 > iconDiff.Length()) + if (label != null) + { + float cursorDist = Vector2.Distance(PlayerInput.MousePosition, iconPos); + if (cursorDist < sprite.size.X * symbolScale) + { + Vector2 textSize = GUIStyle.Font.MeasureString(label); + Vector2 textPos = iconPos + new Vector2(sprite.size.X * symbolScale * 0.7f * Math.Sign(-iconDiff.X), -textSize.Y / 2); + if (iconDiff.X > 0) { textPos.X -= textSize.X; } + DrawString(spriteBatch, textPos + Vector2.One, label, Color.Black); + DrawString(spriteBatch, textPos, label, color); + } + } + + if (screenDist - 10 > iconDiff.Length()) { Vector2 normalizedDiff = Vector2.Normalize(targetScreenPos - iconPos); Vector2 arrowOffset = normalizedDiff * sprite.size.X * symbolScale * 0.7f; @@ -1465,9 +1487,9 @@ namespace Barotrauma depth); } - public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null) + public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit) { - DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font); + DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font, forceUpperCase); } public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index dff86c500..926e5520b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -486,7 +486,7 @@ namespace Barotrauma { if (bounceTimer > 3.0f || bounceDown) { - RectTransform.ScreenSpaceOffset = new Point(RectTransform.ScreenSpaceOffset.X, (int) -(bounceJump * 10f)); + RectTransform.ScreenSpaceOffset = new Point(RectTransform.ScreenSpaceOffset.X, (int) -(bounceJump * 15f * GUI.Scale)); if (!bounceDown) { bounceJump += deltaTime * 4; @@ -503,6 +503,7 @@ namespace Barotrauma bounceJump = 0.0f; bounceTimer = 0.0f; bounceDown = false; + Bounce = false; } } } @@ -730,7 +731,7 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) { return; } - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect); + DrawToolTip(spriteBatch, ToolTip, Rect); } public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) @@ -781,7 +782,7 @@ namespace Barotrauma if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { toolTipBlock.RectTransform.AbsoluteOffset -= new Point( - (targetElement.Width / 2) * Math.Sign(targetElement.Center.X - toolTipBlock.Center.X), + 0, toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); } toolTipBlock.SetTextPos(); @@ -806,9 +807,16 @@ namespace Barotrauma flashColor = (color == null) ? GUIStyle.Red : (Color)color; } - public void FadeOut(float duration, bool removeAfter, float wait = 0.0f) + public void ImmediateFlash(Color? color = null) { - CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait)); + flashTimer = MathHelper.Pi / 4.0f * 0.1f; + flashDuration = 1.0f *0.1f; + flashColor = (color == null) ? GUIStyle.Red : (Color)color; + } + + public void FadeOut(float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null) + { + CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait, onRemove)); } public void FadeIn(float wait, float duration) @@ -870,7 +878,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private IEnumerable LerpAlpha(float to, float duration, bool removeAfter, float wait = 0.0f) + private IEnumerable LerpAlpha(float to, float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null) { State = ComponentState.None; float t = 0.0f; @@ -895,6 +903,7 @@ namespace Barotrauma if (removeAfter && Parent != null) { Parent.RemoveChild(this); + onRemove?.Invoke(); } yield return CoroutineStatus.Success; @@ -1156,7 +1165,7 @@ namespace Barotrauma try { #if USE_STEAM - Steam.SteamManager.OverlayCustomURL(url); + Steam.SteamManager.OverlayCustomUrl(url); #else ToolBox.OpenFileWithShell(url); #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 40eae2afa..3e9166fcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -62,7 +62,7 @@ namespace Barotrauma GUIFont headerFont = GUIStyle.SubHeadingFont; GUIFont font = GUIStyle.SmallFont; // font the context menu options use Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); - int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); + int horizontalPadding = (int)(padding.X + padding.Z), verticalPadding = (int)(padding.Y + padding.W); bool hasHeader = !header.IsNullOrWhiteSpace(); //---------------------------------------------------------------------------------- @@ -131,9 +131,15 @@ namespace Barotrauma optionElement.ToolTip = option.Tooltip; } - if (!option.IsEnabled) + //option doesn't do anything, make it a label + if (option.OnSelected == null) { - optionElement.TextColor *= 0.5f; + optionElement.TextAlignment = Alignment.BottomLeft; + optionElement.TextColor = optionElement.DisabledTextColor = GUIStyle.Green; + } + else if (!option.IsEnabled) + { + optionElement.TextColor *= 0.5f; } } @@ -146,7 +152,10 @@ namespace Barotrauma // 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)); + bool isLabel = block.UserData is ContextMenuOption option && option.OnSelected == null; + block.RectTransform.NonScaledSize = new Point( + (int)(block.TextSize.X + (block.Padding.X + block.Padding.Z)), + (int)Math.Max(block.TextSize.Y * 1.2f, 18 * GUI.Scale)); } int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); @@ -155,7 +164,7 @@ namespace Barotrauma if (HeaderLabel != null) { RectTransform headerTransform = HeaderLabel.RectTransform; - headerTransform.MinSize = new Point((int) (HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y); + headerTransform.MinSize = new Point((int)(HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y); if (largestWidth < headerTransform.MinSize.X) { largestWidth = headerTransform.MinSize.X; @@ -171,7 +180,7 @@ namespace Barotrauma // the cropped size of the option list Point newSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + verticalPadding); // resize the menu itself taking into account the option menus relative Y size - RectTransform.NonScaledSize = new Point(newSize.X, (int) (newSize.Y / optionList.RectTransform.RelativeSize.Y)); + RectTransform.NonScaledSize = new Point(newSize.X, (int)(newSize.Y / optionList.RectTransform.RelativeSize.Y)); optionList.RectTransform.NonScaledSize = newSize; // move the context menu if it would go outside of screen @@ -227,8 +236,8 @@ namespace Barotrauma private Vector2 InflateSize(ref Point size, LocalizedString label, ScalableFont font) { Vector2 textSize = font.MeasureString(label); - size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); - size.Y += (int) Math.Ceiling(textSize.Y); + size.X = Math.Max((int)Math.Ceiling(textSize.X), size.X); + size.Y += (int)Math.Ceiling(textSize.Y); return textSize; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs new file mode 100644 index 000000000..492772759 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs @@ -0,0 +1,97 @@ +using Microsoft.Xna.Framework; +using System; + +namespace Barotrauma +{ + public class GUIDragHandle : GUIComponent + { + private readonly RectTransform elementToMove; + + private Point originalOffset; + + private Vector2 dragStart; + private bool dragStarted; + + public Rectangle DragArea; + + public Func ValidatePosition; + + public bool Dragging => dragStarted; + + public GUIDragHandle(RectTransform rectT, RectTransform elementToMove, string style = "GUIDragIndicator") + : base(style, rectT) + { + enabled = true; + this.elementToMove = elementToMove; + DragArea = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + protected override void Update(float deltaTime) + { + if (!Visible) { return; } + base.Update(deltaTime); + + if (enabled) + { + if (dragStarted) + { + Point moveAmount = (PlayerInput.MousePosition - dragStart).ToPoint() - elementToMove.ScreenSpaceOffset; + Rectangle rect = elementToMove.Rect; + rect.Location += moveAmount; + + moveAmount.X += Math.Max(DragArea.X - rect.X, 0); + moveAmount.X -= Math.Max(rect.Right - DragArea.Right, 0); + moveAmount.Y += Math.Max(DragArea.Y - rect.Y, 0); + moveAmount.Y -= Math.Max(rect.Bottom - DragArea.Bottom, 0); + + if (moveAmount != Point.Zero) + { + elementToMove.ScreenSpaceOffset += moveAmount; + } + + bool isPositionValid = ValidatePosition == null || ValidatePosition.Invoke(elementToMove); + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + if (!isPositionValid) + { + elementToMove.ScreenSpaceOffset = originalOffset; + elementToMove.GUIComponent?.Flash(); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } + dragStarted = false; + } + } + else if (Rect.Contains(PlayerInput.MousePosition) && CanBeFocused && Enabled && GUI.IsMouseOn(this) && !(GUI.MouseOn is GUIButton)) + { + State = Selected ? ComponentState.HoverSelected : ComponentState.Hover; + if (PlayerInput.PrimaryMouseButtonDown()) + { + originalOffset = elementToMove.ScreenSpaceOffset; + dragStart = PlayerInput.MousePosition - elementToMove.ScreenSpaceOffset.ToVector2(); + dragStarted = true; + } + } + else + { + if (!ExternalHighlight) + { + State = Selected ? ComponentState.Selected : ComponentState.None; + } + else + { + State = ComponentState.Hover; + } + } + } + + foreach (GUIComponent child in Children) + { + //allow buttons to handle their states themselves + if (child is GUIButton) { continue; } + child.State = State; + child.Enabled = enabled; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 59510e874..a8ca188cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -204,15 +204,7 @@ namespace Barotrauma currentHighestParent = FindHighestParent(); currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; - rectT.ParentChanged += (RectTransform newParent) => - { - currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; - if (newParent != null) - { - currentHighestParent = FindHighestParent(); - currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; - } - }; + rectT.ParentChanged += _ => RefreshListBoxParent(); } @@ -396,6 +388,15 @@ namespace Barotrauma return true; } + public void RefreshListBoxParent() + { + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; + if (RectTransform.Parent == null) { return; } + + currentHighestParent = FindHighestParent(); + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; + } + private void AddListBoxToGUIUpdateList(GUIComponent parent) { //the parent is not our parent anymore :( @@ -403,11 +404,13 @@ namespace Barotrauma //and somewhere between this component and the higher parent a component was removed for (int i = 1; i < parentHierarchy.Count; i++) { - if (!parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false)) + if (parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false)) { - parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; - return; + continue; } + + parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; + return; } if (Dropped) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index c19bcc378..ea44140e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -85,11 +85,11 @@ namespace Barotrauma get { return sprite; } set { - if (sprite == value) return; + if (sprite == value) { return; } sprite = value; sourceRect = value == null ? Rectangle.Empty : value.SourceRect; origin = value == null ? Vector2.Zero : value.size / 2; - if (scaleToFit) RecalculateScale(); + if (scaleToFit) { RecalculateScale(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 8e43f9c6f..71173d75d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -49,7 +49,7 @@ namespace Barotrauma //scaling the bar linearly with the resolution tends to make them too large on large resolutions float desiredSize = 25.0f; float scaledSize = desiredSize * GUI.Scale; - return (int)((desiredSize + scaledSize) / 2.0f); + return (int)Math.Min((desiredSize + scaledSize) / 2.0f, Rect.Height / 3); } } @@ -57,7 +57,8 @@ namespace Barotrauma { SelectSingle, SelectMultiple, - RequireShiftToSelectMultiple + RequireShiftToSelectMultiple, + None } public SelectMode CurrentSelectMode = SelectMode.SelectSingle; @@ -73,6 +74,8 @@ namespace Barotrauma public bool HideChildrenOutsideFrame = true; + public bool ResizeContentToMakeSpaceForScrollBar = true; + private bool useGridLayout; private GUIComponent scrollToElement; @@ -419,7 +422,7 @@ namespace Barotrauma { dimensionsNeedsRecalculation = false; ContentBackground.RectTransform.Resize(Rect.Size); - bool reduceScrollbarSize = KeepSpaceForScrollBar ? ScrollBarEnabled : ScrollBarVisible; + bool reduceScrollbarSize = ResizeContentToMakeSpaceForScrollBar && (KeepSpaceForScrollBar ? ScrollBarEnabled : ScrollBarVisible); Point contentSize = reduceScrollbarSize ? CalculateFrameSize(ScrollBar.IsHorizontal, ScrollBarSize) : Rect.Size; Content.RectTransform.Resize(new Point((int)(contentSize.X - Padding.X - Padding.Z), (int)(contentSize.Y - Padding.Y - Padding.W))); if (!IsScrollBarOnDefaultSide) { Content.RectTransform.SetPosition(Anchor.BottomRight); } @@ -598,16 +601,13 @@ namespace Barotrauma yield return CoroutineStatus.Success; } float t = 0.0f; - float startScroll = BarScroll * BarSize; + float startScroll = BarScroll; float distanceToTravel = ScrollBar.MaxValue - startScroll; - float progress = startScroll; float speed = distanceToTravel / duration; - - while (t < duration && !MathUtils.NearlyEqual(ScrollBar.MaxValue, progress)) + while (t < duration && !MathUtils.NearlyEqual(ScrollBar.MaxValue, BarScroll)) { t += CoroutineManager.DeltaTime; - progress += speed * CoroutineManager.DeltaTime; - BarScroll = progress; + BarScroll += speed * CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } @@ -1056,7 +1056,7 @@ namespace Barotrauma public void Select(int childIndex, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { - if (childIndex >= Content.CountChildren || childIndex < 0) { return; } + if (childIndex >= Content.CountChildren || childIndex < 0 || CurrentSelectMode == SelectMode.None) { return; } GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } @@ -1155,6 +1155,7 @@ namespace Barotrauma public void Select(IEnumerable children) { + if (CurrentSelectMode == SelectMode.None) { return; } Selected = true; selected.Clear(); selected.AddRange(children.Where(c => Content.Children.Contains(c))); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index fb9756c54..3aa1cff1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -1,9 +1,7 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Networking; using Barotrauma.Extensions; namespace Barotrauma @@ -25,15 +23,18 @@ namespace Barotrauma Default, InGame, Vote, - Hint + Hint, + Tutorial } + private bool IsAnimated => type == Type.InGame || type == Type.Hint || type == Type.Tutorial; + public List Buttons { get; private set; } = new List(); public GUILayoutGroup Content { get; private set; } public GUIFrame InnerFrame { get; private set; } public GUITextBlock Header { get; private set; } public GUITextBlock Text { get; private set; } - public string Tag { get; private set; } + public Identifier Tag { get; private set; } public bool Closed { get; private set; } public bool DisplayInLoadingScreens; @@ -60,6 +61,9 @@ namespace Barotrauma public GUIImage BackgroundIcon { get; private set; } private GUIImage newBackgroundIcon; + /// + /// Close the message box automatically after enough time has passed () + /// public bool AutoClose; private float openState; @@ -69,6 +73,13 @@ namespace Barotrauma private readonly Type type; + /// + /// Close the message box automatically if the condition is met + /// + private readonly Func autoCloseCondition; + + public bool FlashOnAutoCloseCondition { get; set; } + public Type MessageBoxType => type; public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); @@ -79,7 +90,9 @@ namespace Barotrauma this.Buttons[0].OnClicked = Close; } - public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null) + public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, + Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", + Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null, Func autoCloseCondition = null, bool hideCloseButton = false) : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUIStyle.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * type switch @@ -116,6 +129,7 @@ namespace Barotrauma Anchor anchor = type switch { Type.InGame => Anchor.TopCenter, + Type.Tutorial => Anchor.TopCenter, Type.Hint => Anchor.TopRight, Type.Vote => Anchor.TopRight, _ => Anchor.Center @@ -130,7 +144,7 @@ namespace Barotrauma } GUIStyle.Apply(InnerFrame, "", this); this.type = type; - Tag = tag; + Tag = tag.ToIdentifier(); #warning TODO: These should be broken into separate methods at least if (type == Type.Default || type == Type.Vote) @@ -187,12 +201,13 @@ namespace Barotrauma var button = new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f / buttons.Length), buttonContainer.RectTransform), buttons[i]); Buttons.Add(button); } + GUITextBlock.AutoScaleAndNormalize(Buttons.Select(btn => btn.TextBlock)); } - else if (type == Type.InGame) + else if (type == Type.InGame || type == Type.Tutorial) { InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); CanBeFocused = false; - AutoClose = true; + AutoClose = type == Type.InGame; GUIStyle.Apply(InnerFrame, "", this); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), InnerFrame.RectTransform, Anchor.Center), @@ -212,37 +227,43 @@ namespace Barotrauma Content = new GUILayoutGroup(new RectTransform(new Vector2(Icon != null ? 0.65f : 0.85f, 1.0f), horizontalLayoutGroup.RectTransform)); - var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.15f, 1.0f), horizontalLayoutGroup.RectTransform), style: null); - Buttons = new List(1) + if (!hideCloseButton) { - new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), - style: "UIToggleButton") + var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.15f, 1.0f), horizontalLayoutGroup.RectTransform), style: null); + Buttons = new List(1) { - OnClicked = Close - } - }; - - InputType? closeInput = null; - if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) - { - closeInput = InputType.Use; - } - else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) - { - closeInput = InputType.Select; - } - if (closeInput.HasValue) - { - Buttons[0].ToolTip = TextManager.ParseInputTypes($"{TextManager.Get("Close")} ([InputType.{closeInput.Value}])"); - Buttons[0].OnAddedToGUIUpdateList += (GUIComponent component) => - { - if (!closing && openState >= 1.0f && PlayerInput.KeyHit(closeInput.Value)) + new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), + style: "UIToggleButton") { - GUIButton btn = component as GUIButton; - btn?.OnClicked(btn, btn.UserData); - btn?.Flash(GUIStyle.Green); + OnClicked = Close } }; + InputType? closeInput = null; + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) + { + closeInput = InputType.Use; + } + else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) + { + closeInput = InputType.Select; + } + if (closeInput.HasValue) + { + Buttons[0].ToolTip = TextManager.ParseInputTypes($"{TextManager.Get("Close")} ([InputType.{closeInput.Value}])"); + Buttons[0].OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (!closing && openState >= 1.0f && PlayerInput.KeyHit(closeInput.Value)) + { + GUIButton btn = component as GUIButton; + btn?.OnClicked(btn, btn.UserData); + btn?.Flash(GUIStyle.Green); + } + }; + } + } + else + { + Buttons.Clear(); } Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); @@ -274,7 +295,10 @@ namespace Barotrauma Content.RectTransform.NonScaledSize = new Point(Content.Rect.Width, height); } - Buttons[0].RectTransform.MaxSize = new Point((int)(0.4f * Buttons[0].Rect.Y), Buttons[0].Rect.Y); + if (!hideCloseButton) + { + Buttons[0].RectTransform.MaxSize = new Point((int)(0.4f * Buttons[0].Rect.Y), Buttons[0].Rect.Y); + } } else if (type == Type.Hint) { @@ -408,6 +432,8 @@ namespace Barotrauma } } + this.autoCloseCondition = autoCloseCondition; + MessageBoxes.Add(this); } @@ -448,7 +474,7 @@ namespace Barotrauma { // Message box not of type GUIMessageBox is likely the round summary MessageBoxes[i].AddToGUIUpdateList(); - break; + if (!(MessageBoxes[i].UserData is RoundSummary)) { break; } } continue; } @@ -471,6 +497,7 @@ namespace Barotrauma public void SetBackgroundIcon(Sprite icon) { if (icon == null) { return; } + if (icon == BackgroundIcon?.Sprite) { return; } GUIImage newIcon = new GUIImage(new RectTransform(icon.size.ToPoint(), RectTransform), icon) { IgnoreLayoutGroups = true, @@ -510,10 +537,10 @@ namespace Barotrauma } } - if (type == Type.InGame || type == Type.Hint) + if (IsAnimated) { Vector2 initialPos, defaultPos, endPos; - if (type == Type.InGame) + if (type == Type.InGame || type == Type.Tutorial) { initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); @@ -552,6 +579,14 @@ namespace Barotrauma { Close(); } + else if (autoCloseCondition != null && autoCloseCondition()) + { + Close(); + if (FlashOnAutoCloseCondition) + { + InnerFrame.Flash(GUIStyle.Green); + } + } } else { @@ -593,7 +628,7 @@ namespace Barotrauma { newBackgroundIcon.SetAsFirstChild(); newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int)(newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); - newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); + newBackgroundIcon.Color = Color.Lerp(Color.Transparent, Color.White, iconState); if (newBackgroundIcon.Color.A == 255) { BackgroundIcon = newBackgroundIcon; @@ -608,10 +643,9 @@ namespace Barotrauma } } - public void Close() { - if (type == Type.InGame || type == Type.Hint) + if (IsAnimated) { closing = true; } @@ -636,6 +670,19 @@ namespace Barotrauma MessageBoxes.Clear(); } + public static void Close(Identifier tag) + { + foreach (var messageBox in MessageBoxes) + { + if (messageBox is GUIMessageBox mb && mb.Tag == tag) + { + mb.Close(); + } + } + } + + public static void Close(string tag) => Close(tag.ToIdentifier()); + /// /// Parent does not matter. It's overridden. /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 137eee850..8ae5366b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -178,19 +178,19 @@ namespace Barotrauma { public GUIFont(string identifier) : base(identifier) { } - public bool HasValue => Prefabs.Any(); + public bool HasValue => !Prefabs.IsEmpty; public ScalableFont Value => Prefabs.ActivePrefab.Font; public static implicit operator ScalableFont(GUIFont reference) => reference.Value; - public bool ForceUpperCase => HasValue && Value.ForceUpperCase; + public bool ForceUpperCase => Prefabs.ActivePrefab?.Font is { ForceUpperCase: true }; public uint Size => HasValue ? Value.Size : 0; private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); - private ScalableFont GetFontForStr(string str) => + public ScalableFont GetFontForStr(string str) => TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 27be2752f..07e976ada 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -141,11 +141,32 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); + public static Point ItemFrameMargin + { + get + { + Point size = new Point(50, 56).Multiply(GUI.SlicedSpriteScale); + + var style = GetComponentStyle("ItemUI"); + var sprite = style?.Sprites[GUIComponent.ComponentState.None].First(); + if (sprite != null) + { + size.X = Math.Min(sprite.Slices[0].Width + sprite.Slices[2].Width, size.X); + size.Y = Math.Min(sprite.Slices[0].Height + sprite.Slices[6].Height, size.Y); + } + return size; + } + } + public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); - public static GUIComponentStyle GetComponentStyle(string name) - => ComponentStyles.ContainsKey(name) ? ComponentStyles[name] : null; + public static GUIComponentStyle GetComponentStyle(string styleName) + { + return GetComponentStyle(styleName.ToIdentifier()); + } + + public static GUIComponentStyle GetComponentStyle(Identifier identifier) + => ComponentStyles.TryGet(identifier, out var style) ? style : null; public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 404c09927..99461d746 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -587,7 +587,7 @@ namespace Barotrauma if (Shadow) { - Vector2 shadowOffset = new Vector2(GUI.IntScale(2)); + Vector2 shadowOffset = new Vector2(Math.Max(GUI.IntScale(2), 1)); Font.DrawString(spriteBatch, textToShow, pos + shadowOffset, Color.Black, 0.0f, origin, TextScale, SpriteEffects.None, textDepth, alignment: textAlignment, forceUpperCase: ForceUpperCase); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 80d5e820b..1bd3f88eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -412,7 +412,8 @@ namespace Barotrauma return; } - if (MouseRect.Contains(PlayerInput.MousePosition) && (GUI.MouseOn == null || (!(GUI.MouseOn is GUIButton) && GUI.IsMouseOn(this)))) + bool isMouseOn = MouseRect.Contains(PlayerInput.MousePosition) && (GUI.MouseOn == null || (!(GUI.MouseOn is GUIButton) && GUI.IsMouseOn(this))); + if (isMouseOn || isSelecting) { State = ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonDown()) @@ -448,10 +449,6 @@ namespace Barotrauma isSelecting = false; State = ComponentState.None; } - if (!isSelecting) - { - isSelecting = PlayerInput.IsShiftDown(); - } if (mouseHeldInside && !PlayerInput.PrimaryMouseButtonHeld()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 47ce9cab1..8ab3992d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -177,7 +177,7 @@ namespace Barotrauma radioButtonGroup = rbg; } - private void ResizeBox() + public void ResizeBox() { Vector2 textBlockScale = new Vector2(Math.Max(Rect.Width - box.Rect.Width, 0.0f) / Math.Max(Rect.Width, 1.0f), 1.0f); text.RectTransform.RelativeSize = textBlockScale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 1c2e869b0..ded2b870e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -25,6 +25,10 @@ namespace Barotrauma { get; private set; } + public static Rectangle TutorialObjectiveListArea + { + get; private set; + } public static Rectangle MessageAreaTop { @@ -81,6 +85,11 @@ namespace Barotrauma get; private set; } + public static Rectangle ItemHUDArea + { + get; private set; + } + public static int Padding { get; private set; @@ -162,25 +171,35 @@ namespace Barotrauma HealthWindowAreaLeft = new Rectangle(healthWindowX, healthWindowY, healthWindowWidth, healthWindowHeight); + int objectiveListAreaX = HealthWindowAreaLeft.Right + Padding; + int objectiveListAreaY = ButtonAreaTop.Bottom + Padding; + TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (AfflictionAreaLeft.Top - Padding) - objectiveListAreaY); + int votingAreaWidth = (int)(400 * GUI.Scale); int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth; int votingAreaY = Padding + ButtonAreaTop.Height; // Height is based on text content VotingArea = new Rectangle(votingAreaX, votingAreaY, votingAreaWidth, 0); + + ItemHUDArea = new Rectangle(0, ButtonAreaTop.Bottom, GameMain.GraphicsWidth, GameMain.GraphicsHeight - ButtonAreaTop.Bottom - InventoryAreaLower.Height); } public static void Draw(SpriteBatch spriteBatch) { - GUI.DrawRectangle(spriteBatch, ButtonAreaTop, Color.White * 0.5f); - GUI.DrawRectangle(spriteBatch, MessageAreaTop, GUIStyle.Orange * 0.5f); - GUI.DrawRectangle(spriteBatch, CrewArea, Color.Blue * 0.5f); - GUI.DrawRectangle(spriteBatch, ChatBoxArea, Color.Cyan * 0.5f); - GUI.DrawRectangle(spriteBatch, HealthBarArea, Color.Red * 0.5f); - GUI.DrawRectangle(spriteBatch, AfflictionAreaLeft, Color.Red * 0.5f); - GUI.DrawRectangle(spriteBatch, InventoryAreaLower, Color.Yellow * 0.5f); - GUI.DrawRectangle(spriteBatch, HealthWindowAreaLeft, Color.Red * 0.5f); - GUI.DrawRectangle(spriteBatch, BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(ButtonAreaTop, Color.White * 0.5f); + DrawRectangle(TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); + DrawRectangle(MessageAreaTop, GUIStyle.Orange * 0.5f); + DrawRectangle(CrewArea, Color.Blue * 0.5f); + DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); + DrawRectangle(HealthBarArea, Color.Red * 0.5f); + DrawRectangle(AfflictionAreaLeft, Color.Red * 0.5f); + DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); + DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); + DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(ItemHUDArea, Color.Magenta * 0.3f); + + void DrawRectangle(Rectangle r, Color c) => GUI.DrawRectangle(spriteBatch, r, c); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 5a750998d..03a16c643 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -233,7 +233,9 @@ namespace Barotrauma get { Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor); - Point relativeOffset = NonScaledParentRect.MultiplySize(RelativeOffset); + Point relativeOffset = new Point( + (int)(NonScaledParentSize.X * RelativeOffset.X), + (int)(NonScaledParentSize.Y * RelativeOffset.Y)); relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor); return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset; } @@ -256,6 +258,7 @@ namespace Barotrauma public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); protected virtual Rectangle NonScaledUIRect => NonScaledRect; + protected Point NonScaledParentSize => parent?.NonScaledSize ?? new Point(GUI.UIWidth, GameMain.GraphicsHeight); protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect; protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); @@ -336,6 +339,11 @@ namespace Barotrauma public event Action ChildrenChanged; public event Action ScaleChanged; public event Action SizeChanged; + + public void ResetSizeChanged() + { + SizeChanged = null; + } #endregion #region Initialization @@ -730,17 +738,17 @@ namespace Barotrauma get { return animTargetPos ?? AbsoluteOffset; } } - public void MoveOverTime(Point targetPos, float duration) + public void MoveOverTime(Point targetPos, float duration, Action onDoneMoving = null) { animTargetPos = targetPos; - CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration)); + CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration, onDoneMoving)); } public void ScaleOverTime(Point targetSize, float duration) { CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, duration)); } - private IEnumerable DoMoveAnimation(Point targetPos, float duration) + private IEnumerable DoMoveAnimation(Point targetPos, float duration, Action onDoneMoving = null) { Vector2 startPos = AbsoluteOffset.ToVector2(); float t = 0.0f; @@ -752,6 +760,7 @@ namespace Barotrauma } AbsoluteOffset = targetPos; animTargetPos = null; + onDoneMoving?.Invoke(); yield return CoroutineStatus.Success; } private IEnumerable DoScaleAnimation(Point targetSize, float duration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index f32e5de6e..eca7c4334 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -238,7 +238,7 @@ namespace Barotrauma errorId = "Store.SelectStore:StoreDoesntExist"; msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; } - DebugConsole.ShowError(msg); + DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); } } @@ -263,7 +263,7 @@ namespace Barotrauma } if (!msg.IsNullOrEmpty()) { - DebugConsole.ShowError(msg); + DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); } } @@ -1250,7 +1250,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); } } @@ -1808,7 +1808,7 @@ namespace Barotrauma { string errorMsg = $"Error creating a store quantity label text: unknown store tab.\n{e.StackTrace.CleanupStackTrace()}"; #if DEBUG - DebugConsole.ShowError(errorMsg); + DebugConsole.LogError(errorMsg); #else DebugConsole.AddWarning(errorMsg); #endif @@ -1882,7 +1882,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); } if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { @@ -1962,7 +1962,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error adding an item to the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error adding an item to the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } } @@ -1982,7 +1982,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } } @@ -2029,7 +2029,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } var itemsToRemove = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 47d184dd0..7a9a97fc5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -56,6 +56,7 @@ namespace Barotrauma public SubmarineInfo displayedSubmarine; public GUITextBlock submarineName; public GUITextBlock submarineClass; + public GUITextBlock submarineTier; public GUITextBlock submarineFee; public GUIButton selectSubmarineButton; public GUITextBlock middleTextBlock; @@ -162,7 +163,12 @@ namespace Barotrauma IgnoreLayoutGroups = true }; new GUIListBox(new RectTransform(Vector2.One, infoFrame.RectTransform)) { IgnoreLayoutGroups = true, CanBeFocused = false }; - specsFrame = new GUIListBox(new RectTransform(new Vector2(0.39f, 1f), infoFrame.RectTransform), style: null) { Spacing = GUI.IntScale(5), Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding, 0, 0) }; + specsFrame = new GUIListBox(new RectTransform(new Vector2(0.39f, 1f), infoFrame.RectTransform), style: null) + { + CurrentSelectMode = GUIListBox.SelectMode.None, + Spacing = GUI.IntScale(5), + Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding, 0, 0) + }; new GUIFrame(new RectTransform(new Vector2(0.02f, 0.8f), infoFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, style: "VerticalLine"); GUIListBox descriptionFrame = new GUIListBox(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform), style: null) { Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding / 2f) }; descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUIStyle.Font, wrap: true) { CanBeFocused = false }; @@ -220,16 +226,18 @@ namespace Barotrauma }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); - submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); - submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") + submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") { Color = Color.White, HoverColor = Color.White, PressedColor = Color.White }; + submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Left); + submarineDisplayElement.submarineTier = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Right); + submarineDisplays[i] = submarineDisplayElement; } @@ -355,6 +363,7 @@ namespace Barotrauma submarineDisplays[i].submarineName.Text = string.Empty; submarineDisplays[i].submarineFee.Text = string.Empty; submarineDisplays[i].submarineClass.Text = string.Empty; + submarineDisplays[i].submarineTier.Text = string.Empty; submarineDisplays[i].selectSubmarineButton.Enabled = false; submarineDisplays[i].selectSubmarineButton.OnClicked = null; submarineDisplays[i].displayedSubmarine = null; @@ -387,8 +396,13 @@ namespace Barotrauma return true; }; - submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; + submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; + submarineDisplays[i].submarineClass.Text = TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}")); + submarineDisplays[i].submarineClass.ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}.description"); + + submarineDisplays[i].submarineTier.Text = TextManager.Get($"submarinetier.{subToDisplay.Tier}"); + submarineDisplays[i].submarineTier.ToolTip = TextManager.Get("submarinetier.description"); if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { @@ -847,7 +861,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), - ("[currencyname]", currencyName)), messageBoxOptions); + ("[currencyname]", currencyName)) + '\n' + TextManager.Get("submarineswitchinstruction"), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 6bd80ccc1..8a9d4e8c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -206,9 +206,9 @@ namespace Barotrauma transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); - if (Character.Controlled is { } controlled && talentResetButton != null && talentApplyButton != null) + if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null) { - int talentCount = selectedTalents.Count - controlled.Info.GetUnlockedTalentsInTree().Count(); + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) { @@ -403,7 +403,7 @@ namespace Barotrauma CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; case InfoFrameTab.Talents: - CreateTalentInfo(infoFrameHolder); + CreateCharacterInfo(infoFrameHolder); break; } } @@ -631,7 +631,7 @@ namespace Barotrauma linkedGUIList = new List(); - List connectedClients = GameMain.Client.ConnectedClients; + var connectedClients = GameMain.Client.ConnectedClients; for (int i = 0; i < teamIDs.Count; i++) { @@ -1722,7 +1722,11 @@ namespace Barotrauma var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); - LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? + TextManager.GetWithVariables("submarine.classandtier", + ("[class]", TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")), + ("[tier]", TextManager.Get($"submarinetier.{sub.Info.Tier}"))) : + TextManager.Get("shuttle"); int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; @@ -1763,7 +1767,10 @@ namespace Barotrauma } else { - var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft)); + var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft)) + { + CurrentSelectMode = GUIListBox.SelectMode.None + }; sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); } } @@ -1803,162 +1810,126 @@ namespace Barotrauma { TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) }, }.ToImmutableDictionary(); - private void CreateTalentInfo(GUIFrame infoFrame) + private void CreateCharacterInfo(GUIFrame infoFrame) { infoFrame.ClearChildren(); talentButtons.Clear(); talentCornerIcons.Clear(); - GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = GUI.IntScale(15); - GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); + GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); - GUIFrame paddedTalentFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), talentFrameContent.RectTransform, Anchor.Center), style: null); - - GUIFrame talentFrameMain = new GUIFrame(new RectTransform(Vector2.One, paddedTalentFrame.RectTransform), style: null); + GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null); GUIFrame characterSettingsFrame = null; GUILayoutGroup characterLayout = null; if (!(GameMain.NetworkMember is null)) { - characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrameContent.RectTransform), style: null) { Visible = false }; + characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false }; characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); } - /*Character controlledCharacter = Character.Controlled; - if (controlledCharacter == null) { return; } - - if (controlledCharacter.Info is null) - { - DebugConsole.ThrowError("No character info found for talent UI"); - return; - }*/ - Character controlledCharacter = Character.Controlled; CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; if (info == null) { return; } Job job = info.Job; - GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameMain.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) { - AbsoluteSpacing = GUI.IntScale(5) + AbsoluteSpacing = GUI.IntScale(10), + Stretch = true }; - GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); + GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true); - - new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), talentInfoLayoutGroup.RectTransform), onDraw: (batch, component) => + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => { float posY = component.Rect.Center.Y - component.Rect.Width / 2; info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); }); - GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; + GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) + { + AbsoluteSpacing = GUI.IntScale(5), + CanBeFocused = true + }; - Vector2 nameSize = GUIStyle.SubHeadingFont.MeasureString(info.Name); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); - nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); if (!info.OmitJobInMenus) { nameBlock.TextColor = job.Prefab.UIColor; - Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; - jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; } - LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); + LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName); Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - GUIFrame talentsOutsideTreeFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); - - if (!(GameMain.NetworkMember is null)) + IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); + if (talentsOutsideTree.Count() > 0) { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), talentsOutsideTreeFrame.RectTransform, Anchor.TopLeft), - text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null); + + GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont); + talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); + + var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) { - IgnoreLayoutGroups = true + AutoHideScrollBar = false, + ResizeContentToMakeSpaceForScrollBar = false }; - newCharacterBox.TextBlock.AutoScaleHorizontal = true; + extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); + extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65)); + extraTalentLayout.Recalculate(); + extraTalentList.ForceLayoutRecalculation(); - newCharacterBox.OnClicked = (button, o) => + foreach (var extraTalent in talentsOutsideTree) { - if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true) { - GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => - { - newCharacterBox.Text = TextManager.Get("settings"); - - if (pendingChangesFrame != null) - { - NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); - } - OpenMenu(); - }); - return true; - } - - OpenMenu(); - return true; - - void OpenMenu() - { - characterSettingsFrame!.Visible = true; - talentFrameMain.Visible = false; - } - }; - - if (!(characterLayout is null)) - { - GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomRight); - new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? - { - OnClicked = (button, o) => - { - GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); - characterSettingsFrame!.Visible = false; - talentFrameMain.Visible = true; - return true; - } + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description), + Color = GUIStyle.Green }; + img.RectTransform.SizeChanged += () => + { + img.RectTransform.MaxSize = new Point(img.Rect.Height); + }; } } - IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); + GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight) + { + AbsoluteSpacing = GUI.IntScale(5), + Stretch = true + }; - if (talentsOutsideTree.Count() > 0) - { - //TODO: replace with something more generic - GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), talentsOutsideTreeFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") - { - ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", talentsOutsideTree.Select(e => e.DisplayName))}" - }; - } - - GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; - - LocalizedString skillString = TextManager.Get("skills"); - Vector2 skillSize = GUIStyle.SubHeadingFont.MeasureString(skillString); - GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUIStyle.SubHeadingFont); - skillBlock.RectTransform.NonScaledSize = skillSize.Pad(skillBlock.Padding).ToPoint(); + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont); skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); - CreateTalentSkillList(controlledCharacter, info, skillListBox); + CreateSkillList(controlledCharacter, info, skillListBox); - if (controlledCharacter != null) + new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); + + GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + + if (controlledCharacter == null) + { + talentTreeListBox.Enabled = false; + } + else { if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); - - GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); - selectedTalents = info.GetUnlockedTalentsInTree().ToList(); List subTreeNames = new List(); @@ -1994,24 +1965,24 @@ namespace Barotrauma Point iconSize = cornerIcon.RectTransform.NonScaledSize; cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); - if (subTree.TalentOptionStages.Count <= i) { continue; } + if (subTree.TalentOptionStages.Length <= i) { continue; } TalentOption talentOption = subTree.TalentOptionStages[i]; GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - foreach (TalentPrefab talent in talentOption.Talents.OrderBy(t => t.Identifier)) + foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t)) { + if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; } GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) { CanBeFocused = false }; GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); - GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { - ToolTip = RichString.Rich(talent.DisplayName + "\n\n" + talent.Description), + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description), UserData = talent.Identifier, PressedColor = pressedColor, Enabled = controlledCharacter != null, @@ -2078,9 +2049,13 @@ namespace Barotrauma } GUITextBlock.AutoScaleAndNormalize(subTreeNames); - GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true) + { + RelativeSpacing = 0.01f, + Stretch = true + }; - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform)); GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), @@ -2097,47 +2072,100 @@ namespace Barotrauma talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; - talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") { OnClicked = ResetTalentSelection }; - talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") { OnClicked = ApplyTalentSelection, }; GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); } + if (!(GameMain.NetworkMember is null)) + { + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") + { + IgnoreLayoutGroups = false + }; + newCharacterBox.TextBlock.AutoScaleHorizontal = true; + + newCharacterBox.OnClicked = (button, o) => + { + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + if (pendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + } + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + content.Visible = false; + } + }; + + if (!(characterLayout is null)) + { + GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); + new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? + { + OnClicked = (button, o) => + { + GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); + characterSettingsFrame!.Visible = false; + content.Visible = true; + return true; + } + }; + } + } + UpdateTalentInfo(); } - private void CreateTalentSkillList(Character character, CharacterInfo info, GUIListBox parent) + private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) { parent.Content.ClearChildren(); List skillNames = new List(); foreach (Skill skill in info.Job.GetSkills()) { - GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; + GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true }; + var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value)); + skillNames.Add(skillName); + skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height); + skillContainer.RectTransform.MinSize = new Point(0, skillName.Rect.Height); - skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value))); - new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight); float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); - //TODO: if/when we upgrade to C# 9, do neater pattern matching here - string stringColor = true switch + string stringColor = skillChange switch { - true when skillChange > 0 => XMLExtensions.ColorToString(GUIStyle.Green), - true when skillChange < 0 => XMLExtensions.ColorToString(GUIStyle.Red), - _ => XMLExtensions.ColorToString(GUIStyle.TextColorNormal) + > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), + < 0 => XMLExtensions.ToStringHex(GUIStyle.Red), + _ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal) }; RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } - skillContainer.Recalculate(); + //skillContainer.Recalculate(); } parent.RecalculateChildren(); @@ -2216,7 +2244,7 @@ namespace Barotrauma talentButton.icon.HoverColor = hoverColor; } - CreateTalentSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); + CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); } private void ApplyTalents(Character controlledCharacter) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 4d5f3f806..08a186081 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -17,7 +17,7 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { - internal class UpgradeStore + internal sealed class UpgradeStore { public readonly struct CategoryData { @@ -432,13 +432,21 @@ namespace Barotrauma }; Location location = Campaign.Map.CurrentLocation; - int hullRepairCost = location?.GetAdjustedMechanicalCost(CampaignMode.HullRepairCost) ?? CampaignMode.HullRepairCost; - int itemRepairCost = location?.GetAdjustedMechanicalCost(CampaignMode.ItemRepairCost) ?? CampaignMode.ItemRepairCost; - int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(CampaignMode.ShuttleReplaceCost) ?? CampaignMode.ShuttleReplaceCost; + + int hullRepairCost = Campaign.GetHullRepairCost(); + int itemRepairCost = Campaign.GetItemRepairCost(); + int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; + if (location != null) + { + hullRepairCost = location.GetAdjustedMechanicalCost(hullRepairCost); + itemRepairCost = location.GetAdjustedMechanicalCost(itemRepairCost); + shuttleRetrieveCost = location.GetAdjustedMechanicalCost(shuttleRetrieveCost); + } CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallwalls"), "RepairHullButton", hullRepairCost, (button, o) => { - if (Campaign.PurchasedHullRepairs) + //cost is zero = nothing to repair + if (Campaign.PurchasedHullRepairs || hullRepairCost <= 0) { button.Enabled = false; return false; @@ -471,7 +479,7 @@ namespace Barotrauma return false; } return true; - }, Campaign.PurchasedHullRepairs || !HasPermission, isHovered => + }, Campaign.PurchasedHullRepairs || !HasPermission || hullRepairCost <= 0, isHovered => { highlightWalls = isHovered; return true; @@ -479,7 +487,8 @@ namespace Barotrauma CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => { - if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs) + //cost is zero = nothing to repair + if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs && itemRepairCost > 0) { LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => @@ -505,9 +514,8 @@ namespace Barotrauma button.Enabled = false; return false; } - return true; - }, Campaign.PurchasedItemRepairs || !HasPermission, isHovered => + }, Campaign.PurchasedItemRepairs || !HasPermission || itemRepairCost <= 0, isHovered => { foreach (var (item, itemFrame) in itemPreviews) { @@ -839,7 +847,8 @@ namespace Barotrauma foreach (UpgradePrefab prefab in prefabs) { - CreateUpgradeEntry(prefab, category, parent.Content, entitiesOnSub); + if (prefab.MaxLevel is 0) { continue; } + CreateUpgradeEntry(prefab, category, parent.Content, submarine, entitiesOnSub); } } @@ -865,7 +874,7 @@ namespace Barotrauma itemPrefab.SwappableItem.CanBeBought && itemPrefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)).Cast(); - var linkedItems = Campaign.UpgradeManager.GetLinkedItemsToSwap(item) ?? new List() { item }; + var linkedItems = UpgradeManager.GetLinkedItemsToSwap(item) ?? new List() { item }; //create the swap entry only for one of the items (the one with the smallest ID) if (linkedItems.Min(it => it.ID) < item.ID) { return; } @@ -901,7 +910,7 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true); LocalizedString slotText = ""; - if (linkedItems.Count > 1) + if (linkedItems.Count() > 1) { slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString()))); } @@ -973,7 +982,7 @@ namespace Barotrauma bool isPurchased = item.AvailableSwaps.Contains(replacement); - int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count; + int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, price, replacement, @@ -989,7 +998,7 @@ namespace Barotrauma { LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", ("[itemtoinstall]", replacement.Name), - ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString())); + ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count()).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1033,7 +1042,7 @@ namespace Barotrauma } if (toggleButton.Selected) { - var linkedItems = Campaign.UpgradeManager.GetLinkedItemsToSwap(item); + var linkedItems = UpgradeManager.GetLinkedItemsToSwap(item); foreach (var itemPreview in itemPreviews) { itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUIStyle.Orange : previewWhite; @@ -1104,7 +1113,7 @@ namespace Barotrauma GUILayoutGroup? buyButtonLayout = null; if (addProgressBar) - { + { progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; @@ -1116,7 +1125,11 @@ namespace Barotrauma //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); + var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center) + { + //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades + Visible = userData is ItemPrefab + }; if (price < 0) { priceText.TextColor = GUIStyle.Green; @@ -1175,9 +1188,10 @@ namespace Barotrauma } } - private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List? itemsOnSubmarine) + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, Submarine submarine, List? itemsOnSubmarine) { - if (Campaign is null) { return; } + Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; + if (Campaign is null || sub is null) { return; } GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); var prefabLayout = prefabFrame.GetChild(); @@ -1193,7 +1207,7 @@ namespace Barotrauma var buyButtonLayout = childLayouts[2]; var buyButton = buyButtonLayout.GetChild(); - if (!HasPermission || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) + if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) { prefabFrame.Enabled = false; description.Enabled = false; @@ -1243,12 +1257,17 @@ namespace Barotrauma List upgrades = entity.GetUpgrades(); int upgradesCount = upgrades.Count; const int maxUpgrades = 4; - - itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); + + Item? item = entity as Item; + itemName.Text = item?.Name ?? TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); } + if (item?.PendingItemSwap != null) + { + itemName.Text = RichString.Rich(itemName.Text + "\n" + TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", item.PendingItemSwap.Name)); + } upgradeList.Content.ClearChildren(); for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) { @@ -1261,7 +1280,7 @@ namespace Barotrauma // include pending upgrades into the tooltip foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades) { - if (entity is Item item && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) + if (item != null && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) { bool found = false; foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast()) @@ -1305,6 +1324,8 @@ namespace Barotrauma Item[] entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToArray(); foreach (UpgradeCategory category in UpgradeCategory.Categories) { + //hide categories with no upgrades in them + if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category))) { continue; } if (entitiesOnSub.Any(item => category.CanBeApplied(item, null))) { yield return category; @@ -1370,12 +1391,16 @@ namespace Barotrauma { if (selectedUpgradeCategoryLayout != null) { - var linkedItems = HoveredEntity is Item hoveredItem ? Campaign.UpgradeManager.GetLinkedItemsToSwap(hoveredItem) : new List(); + var linkedItems = HoveredEntity is Item hoveredItem ? UpgradeManager.GetLinkedItemsToSwap(hoveredItem) : new List(); if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData is Item item && (item == HoveredEntity || linkedItems.Contains(item)), recursive: true) is GUIButton itemElement) { if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); } + else + { + ScrollToCategory(data => data.Category.CanBeApplied(item, null)); + } } } else @@ -1436,7 +1461,7 @@ namespace Barotrauma * |--------------------------------------------------| * | name | * |--------------------------------------------------| - * | class | + * | class + tier | * |--------------------------------------------------| * | description | * | | @@ -1446,13 +1471,24 @@ namespace Barotrauma submarineInfoFrame = new GUILayoutGroup(rectT(0.25f, 0.2f, mainStoreLayout, Anchor.TopRight)) { IgnoreLayoutGroups = true }; // submarine name new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont); - // submarine class - new GUITextBlock(rectT(1, 0, submarineInfoFrame), $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}", textAlignment: Alignment.Right, font: GUIStyle.Font); + + LocalizedString classText = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}"; + // submarine class + tier + new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), classText, textAlignment: Alignment.Right, font: GUIStyle.Font) + { + ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description") + }; + new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), TextManager.Get($"submarinetier.{submarine.Info.Tier}"), textAlignment: Alignment.Right, font: GUIStyle.Font) + { + ToolTip = TextManager.Get("submarinetier.description") + }; + var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true); submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale)); - + description.Padding = new Vector4(description.Padding.X, 24 * GUI.Scale, description.Padding.Z, description.Padding.W); - List pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs) where category.CanBeApplied(item, null) && item.IsPlayerTeamInteractable select item).Cast().ToList(); + List pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs) + where category.CanBeApplied(item, null) && item.IsPlayerTeamInteractable select item).Cast().Distinct().ToList(); List ids = GameMain.GameSession.SubmarineInfo?.LeftBehindDockingPortIDs ?? new List(); pointsOfInterest.AddRange(submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs).Where(item => ids.Contains(item.ID))); @@ -1600,6 +1636,7 @@ namespace Barotrauma List textBlocks = buttonParent.GetAllChildren().ToList(); GUITextBlock priceLabel = textBlocks[0]; + priceLabel.Visible = true; int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); if (priceLabel != null && !WaitForServerUpdate) @@ -1638,9 +1675,15 @@ namespace Barotrauma IEnumerable applicableCategories) { // Disables the parent and only re-enables if the submarine contains valid items - if (!category.IsWallUpgrade && drawnSubmarine != null) + if (!category.IsWallUpgrade && drawnSubmarine?.Info != null) { - if (applicableCategories.Contains(category)) + if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category) && p.GetMaxLevel(drawnSubmarine.Info) > 0)) + { + parent.ToolTip = TextManager.Get("upgradecategorynotapplicable"); + parent.Enabled = false; + parent.SelectedColor = GUIStyle.Red * 0.5f; + } + else if (applicableCategories.Contains(category)) { parent.Enabled = true; parent.SelectedColor = parent.Style.SelectedColor; @@ -1660,14 +1703,27 @@ namespace Barotrauma { if (component.UserData != prefab) { continue; } + if (prefab.MaxLevel is 0) + { + component.Visible = false; + continue; + } + Dictionary styles = GUIStyle.GetComponentStyle("upgradeindicator").ChildStyles; if (!styles.ContainsKey("upgradeindicatoron") || !styles.ContainsKey("upgradeindicatordim") || !styles.ContainsKey("upgradeindicatoroff")) { continue; } GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()]; GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()]; GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()]; + int maxLevel = prefab.MaxLevel; - if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) + if (maxLevel == 0) + { + SetOff(); + continue; + } + + if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= maxLevel) { // we check this to avoid flickering from re-applying the same style if (image.Style == onStyle) { continue; } @@ -1680,25 +1736,42 @@ namespace Barotrauma } else { - if (image.Style == offStyle) { continue; } + SetOff(); + } + + void SetOff() + { + if (image.Style == offStyle) { return; } image.ApplyStyle(offStyle); } } } } - + private void ScrollToCategory(Predicate predicate, GUIListBox.PlaySelectSound playSelectSound = GUIListBox.PlaySelectSound.No) { if (currentStoreLayout == null) { return; } + CategoryData? mostAppropriateCategory = null; + GUIComponent? mostAppropriateChild = null; foreach (GUIComponent child in currentStoreLayout.Content.Children) { if (child.UserData is CategoryData data && predicate(data)) { - currentStoreLayout.ScrollToElement(child, playSelectSound); - break; + //choose the category with least items in it as the "most appropriate" + //e.g. when selecting junction boxes, we want to select the "junction boxes" category instead of "electrical repairs" which contains many electrical devices + if (mostAppropriateCategory == null || + data.Category.ItemTags.Count() < mostAppropriateCategory.Value.Category.ItemTags.Count()) + { + mostAppropriateCategory = data; + mostAppropriateChild = child; + } } } + if (mostAppropriateChild != null) + { + currentStoreLayout.ScrollToElement(mostAppropriateChild, playSelectSound); + } } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index 5b596f54d..6152fa2dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -131,9 +131,9 @@ namespace Barotrauma LoadContent(contentPath, videoSettings, textSettings, contentId, startPlayback, new RawLString(""), null); } - public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback, LocalizedString objective, Action callback = null) + public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback, LocalizedString objective, Action onStop = null) { - callbackOnStop = callback; + callbackOnStop = onStop; filePath = contentPath + videoSettings.File; if (!File.Exists(filePath)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 7d83f6e99..6afb4c50f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -66,7 +66,7 @@ namespace Barotrauma { currentVoteType = type; CreateVotingGUI(); - if (starter.ID == GameMain.Client.ID) { SetGUIToVotedState(2); } + if (starter.SessionId == GameMain.Client.SessionId) { SetGUIToVotedState(2); } VoteRunning = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index bbe4aff5a..6016a10c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -111,9 +111,7 @@ namespace Barotrauma private readonly GameTime fixedTime; - public string ConnectName; - public string ConnectEndpoint; - public UInt64 ConnectLobby; + public Option ConnectCommand = Option.None(); private static SpriteBatch spriteBatch; @@ -236,20 +234,14 @@ namespace Barotrauma ConsoleArguments = args; - ConnectName = null; - ConnectEndpoint = null; - ConnectLobby = 0; - try { - ToolBox.ParseConnectCommand(ConsoleArguments, out ConnectName, out ConnectEndpoint, out ConnectLobby); + ConnectCommand = ToolBox.ParseConnectCommand(ConsoleArguments); } catch (IndexOutOfRangeException e) { DebugConsole.ThrowError($"Failed to parse console arguments ({string.Join(' ', ConsoleArguments)})", e); - ConnectName = null; - ConnectEndpoint = null; - ConnectLobby = 0; + ConnectCommand = Option.None(); } GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); @@ -473,7 +465,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; - UgcTransition.Prepare(); + LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); foreach (var progress in contentPackageLoadRoutine) { @@ -569,6 +561,8 @@ namespace Barotrauma new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); } + GameSettings.OnGameMainHasLoaded?.Invoke(); + TitleScreen.LoadState = 100.0f; HasLoaded = true; if (GameSettings.CurrentConfig.VerboseLogging) @@ -604,7 +598,7 @@ namespace Barotrauma { try { - ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand), out ConnectName, out ConnectEndpoint, out ConnectLobby); + ConnectCommand = ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand)); } catch (IndexOutOfRangeException e) { @@ -613,12 +607,8 @@ namespace Barotrauma #else DebugConsole.Log($"Failed to parse a Steam friend's connect invitation command ({connectCommand})\n" + e.StackTrace.CleanupStackTrace()); #endif - ConnectName = null; - ConnectEndpoint = null; - ConnectLobby = 0; + ConnectCommand = Option.None(); } - - DebugConsole.NewMessage(ConnectName + ", " + ConnectEndpoint, Color.Yellow); } public void OnLobbyJoinRequested(Steamworks.Data.Lobby lobby, Steamworks.SteamId friendId) @@ -734,7 +724,7 @@ namespace Barotrauma } #endif - NetworkMember?.Update((float)Timing.Step); + Client?.Update((float)Timing.Step); if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { @@ -743,38 +733,29 @@ namespace Barotrauma } else if (HasLoaded) { - if (ConnectLobby != 0) + if (ConnectCommand is Some { Value: var connectCommand }) { if (Client != null) { - Client.Disconnect(); + Client.Quit(); Client = null; - - GameMain.MainMenuScreen.Select(); + MainMenuScreen.Select(); } - Steam.SteamManager.JoinLobby(ConnectLobby, true); - ConnectLobby = 0; - ConnectEndpoint = null; - ConnectName = null; - } - else if (!string.IsNullOrWhiteSpace(ConnectEndpoint)) - { - if (Client != null) + if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) { - Client.Disconnect(); - Client = null; - - GameMain.MainMenuScreen.Select(); + SteamManager.JoinLobby(lobbyId, joinServer: true); } - UInt64 serverSteamId = SteamManager.SteamIDStringToUInt64(ConnectEndpoint); - Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), - serverSteamId != 0 ? null : ConnectEndpoint, - serverSteamId, - string.IsNullOrWhiteSpace(ConnectName) ? ConnectEndpoint : ConnectName); - ConnectLobby = 0; - ConnectEndpoint = null; - ConnectName = null; + else if (connectCommand.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint) + && nameAndEndpoint is { ServerName: var serverName, Endpoint: var endpoint }) + { + Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), + endpoint, + string.IsNullOrWhiteSpace(serverName) ? endpoint.StringRepresentation : serverName, + Option.None()); + } + + ConnectCommand = Option.None(); } SoundPlayer.Update((float)Timing.Step); @@ -823,7 +804,7 @@ namespace Barotrauma else if ((Character.Controlled == null || !itemHudActive()) && CharacterHealth.OpenHealthWindow == null && !CrewManager.IsCommandInterfaceOpen - && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedConstruction != null)) + && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedItem != null)) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); @@ -831,9 +812,9 @@ namespace Barotrauma static bool itemHudActive() { - if (Character.Controlled?.SelectedConstruction == null) { return false; } + if (Character.Controlled?.SelectedItem == null) { return false; } return - Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null) || + Character.Controlled.SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null) || ((Character.Controlled.ViewTarget as Item)?.Prefab?.FocusOnSelected ?? false); } } @@ -902,7 +883,7 @@ namespace Barotrauma } } - NetworkMember?.Update((float)Timing.Step); + Client?.Update((float)Timing.Step); GUI.Update((float)Timing.Step); @@ -928,7 +909,7 @@ namespace Barotrauma #endif } - CoroutineManager.Update((float)Timing.Step, Paused ? 0.0f : (float)Timing.Step); + CoroutineManager.Update(Paused, (float)Timing.Step); SteamManager.Update((float)Timing.Step); @@ -1102,7 +1083,7 @@ namespace Barotrauma if (Client != null) { - Client.Disconnect(); + Client.Quit(); Client = null; } @@ -1118,7 +1099,7 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); foreach (var activeEvent in GameSession.EventManager.ActiveEvents) { - GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.ToString()); + GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.Prefab.Identifier); } GameSession.LogEndRoundStats(eventId); if (GameSession.GameMode is TutorialMode tutorialMode) @@ -1184,7 +1165,7 @@ namespace Barotrauma UserData = "https://steamcommunity.com/app/602960/discussions/1/", OnClicked = (btn, userdata) => { - if (!SteamManager.OverlayCustomURL(userdata as string)) + if (!SteamManager.OverlayCustomUrl(userdata as string)) { ShowOpenUrlInWebBrowserPrompt(userdata as string); } @@ -1222,7 +1203,7 @@ namespace Barotrauma { exiting = true; DebugConsole.NewMessage("Exiting..."); - NetworkMember?.Disconnect(); + Client?.Quit(); SteamManager.ShutDown(); try diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 07ed155ff..dd4ca114b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -103,7 +103,7 @@ namespace Barotrauma { if (soldItem.ItemPrefab != soldEntity.ItemPrefab) { return false; } if (matchId && (soldEntity.Item == null || soldItem.ID != soldEntity.Item.ID)) { return false; } - if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.ID) { return false; } + if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.SessionId) { return false; } return true; } } @@ -139,16 +139,16 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; - byte sellerId = GameMain.Client?.ID ?? 0; + byte sellerId = GameMain.Client?.SessionId ?? 0; // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); if (!(Location.GetStore(storeIdentifier) is { } store)) { - DebugConsole.ShowError($"Error selling items at {Location}: no store with identifier \"{storeIdentifier}\" exists.\n{Environment.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error selling items at {Location}: no store with identifier \"{storeIdentifier}\" exists.\n{Environment.StackTrace.CleanupStackTrace()}"); return; } var storeSpecificSoldItems = GetSoldItems(storeIdentifier, create: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index a4f756646..5820ca4cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -183,14 +183,13 @@ namespace Barotrauma { 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") { - ToolTip = TextManager.Get("chat"), + ToolTip = TextManager.GetWithVariable("hudbutton.chatbox", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ChatBox)), ClampMouseRectToParent = false }; chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { - chatBox.ToggleOpen = !chatBox.ToggleOpen; - chatBox.CloseAfterMessageSent = false; + chatBox.Toggle(); return true; }; } @@ -292,29 +291,13 @@ namespace Barotrauma return crewArea.Rect; } - /// - /// Remove the character from the crew (and crew menus). - /// - /// The character to remove - /// If the character info is also removed, the character will not be visible in the round summary. - public void RemoveCharacter(Character character, bool removeInfo = false, bool resetCrewListIndex = true) - { - if (character == null) - { - DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - characters.Remove(character); - if (removeInfo) { characterInfos.Remove(character.Info); } - if (resetCrewListIndex) { ResetCrewListIndex(character); } - } - /// /// Add character to the list without actually adding it to the crew /// public GUIComponent AddCharacterToCrewList(Character character) { if (character == null) { return null; } + if (crewList.Content.Children.Any(c => c.UserData as Character == character)) { return null; } var background = new GUIFrame( new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), @@ -510,7 +493,15 @@ namespace Barotrauma return background; } - private void SetCharacterComponentTooltip(GUIComponent characterComponent) + public void RemoveCharacterFromCrewList(Character character) + { + if (crewList?.Content.GetChildByUserData(character) is { } component) + { + crewList.RemoveChild(component); + } + } + + private static void SetCharacterComponentTooltip(GUIComponent characterComponent) { if (!(characterComponent?.UserData is Character character)) { return; } if (character.Info?.Job?.Prefab == null) { return; } @@ -1390,7 +1381,7 @@ namespace Barotrauma } else { - CreateCommandUI(HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + CreateCommandUI(CharacterHUD.MouseOnCharacterPortrait() ? Character.Controlled : GUI.MouseOn?.UserData as Character); } SoundPlayer.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; @@ -2412,7 +2403,7 @@ namespace Barotrauma float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor // --> Create shortcut node for "Operate Reactor" order's "Power Up" option - if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) + if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedItem == reactor.Item)) { var orderPrefab = OrderPrefab.Prefabs["operatereactor"]; var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor); @@ -2426,7 +2417,7 @@ namespace Barotrauma // 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 (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && - subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); @@ -2822,7 +2813,7 @@ namespace Barotrauma return node; } - private struct MinimapNodeData + public struct MinimapNodeData { public Order Order; } @@ -3517,9 +3508,9 @@ namespace Barotrauma if (node == null || characterContext != null) { return false; } if (node.UserData is Order nodeOrder) { - return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && - (!nodeOrder.MustSetTarget || itemContext != null || - nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + return !nodeOrder.TargetAllCharacters && + (!nodeOrder.Prefab.HasOptions || !nodeOrder.Option.IsEmpty) && + (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameMode.cs index 83b87c5ce..a3c45a280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameMode.cs @@ -3,8 +3,8 @@ namespace Barotrauma { partial class GameMode { - public virtual void Draw(SpriteBatch spriteBatch) - { - } + public virtual void HUDScaleChanged() { } + + public virtual void Draw(SpriteBatch spriteBatch) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index c7d279a2d..94ba1b79a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -115,12 +115,16 @@ namespace Barotrauma partial void InitProjSpecific() { - var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUI.Canvas), - isHorizontal: true, childAnchor: Anchor.CenterRight) - { - CanBeFocused = false - }; + CreateButtons(); + } + public override void HUDScaleChanged() + { + CreateButtons(); + } + + private void CreateButtons() + { int buttonHeight = (int) (GUI.Scale * 40), buttonWidth = GUI.IntScale(450), buttonCenter = buttonHeight / 2, @@ -166,8 +170,6 @@ namespace Barotrauma }, UserData = "ReadyCheckButton" }; - - buttonContainer.Recalculate(); } private void InitCampaignUI() @@ -311,7 +313,7 @@ namespace Barotrauma if (prevControlled != null) { - prevControlled.SelectedConstruction = null; + prevControlled.SelectedItem = prevControlled.SelectedSecondaryItem = null; if (prevControlled.AIController != null) { prevControlled.AIController.Enabled = true; @@ -362,7 +364,7 @@ namespace Barotrauma float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { - t += CoroutineManager.UnscaledDeltaTime; + t += CoroutineManager.DeltaTime; overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); yield return CoroutineStatus.Running; } @@ -469,7 +471,6 @@ namespace Barotrauma { base.End(transitionType); ForceMapUI = ShowCampaignUI = false; - UpgradeManager.CanUpgrade = true; // remove all event dialogue boxes GUIMessageBox.MessageBoxes.ForEachMod(mb => @@ -539,37 +540,37 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); - msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + msg.WriteUInt16(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); + msg.WriteUInt16(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); var selectedMissionIndices = map.GetSelectedMissionIndices(); - msg.Write((byte)selectedMissionIndices.Count()); + msg.WriteByte((byte)selectedMissionIndices.Count()); foreach (int selectedMissionIndex in selectedMissionIndices) { - msg.Write((byte)selectedMissionIndex); + msg.WriteByte((byte)selectedMissionIndex); } - msg.Write(PurchasedHullRepairs); - msg.Write(PurchasedItemRepairs); - msg.Write(PurchasedLostShuttles); + msg.WriteBoolean(PurchasedHullRepairs); + msg.WriteBoolean(PurchasedItemRepairs); + msg.WriteBoolean(PurchasedLostShuttles); WriteItems(msg, CargoManager.ItemsInBuyCrate); WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); WriteItems(msg, CargoManager.PurchasedItems); WriteItems(msg, CargoManager.SoldItems); - msg.Write((ushort)UpgradeManager.PurchasedUpgrades.Count); + msg.WriteUInt16((ushort)UpgradeManager.PurchasedUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades) { - msg.Write(prefab.Identifier); - msg.Write(category.Identifier); - msg.Write((byte)level); + msg.WriteIdentifier(prefab.Identifier); + msg.WriteIdentifier(category.Identifier); + msg.WriteByte((byte)level); } - msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + msg.WriteUInt16((ushort)UpgradeManager.PurchasedItemSwaps.Count); foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { - msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + msg.WriteUInt16(itemSwap.ItemToRemove.ID); + msg.WriteIdentifier(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } } @@ -760,6 +761,7 @@ namespace Barotrauma item.PendingItemSwap = null; } } + campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index fc3f8f71c..dc7bc12b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -1,12 +1,8 @@ -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -49,6 +45,31 @@ namespace Barotrauma private bool showCampaignResetText; + public override bool PurchasedHullRepairs + { + get { return PurchasedHullRepairsInLatestSave; } + set + { + PurchasedHullRepairsInLatestSave = value; + } + } + public override bool PurchasedLostShuttles + { + get { return PurchasedLostShuttlesInLatestSave; } + set + { + PurchasedLostShuttlesInLatestSave = value; + } + } + public override bool PurchasedItemRepairs + { + get { return PurchasedItemRepairsInLatestSave; } + set + { + PurchasedItemRepairsInLatestSave = value; + } + } + #region Constructors/initialization /// @@ -132,9 +153,9 @@ namespace Barotrauma }; } - PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); - PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); - PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); + PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false); + PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false); + PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false); CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); if (CheatsEnabled) { @@ -169,10 +190,20 @@ namespace Barotrauma public static SinglePlayerCampaign Load(XElement element) => new SinglePlayerCampaign(element); private void InitUI() + { + CreateEndRoundButton(); + + campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); + CampaignUI = new CampaignUI(this, campaignUIContainer) + { + StartRound = () => { TryEndRound(); } + }; + } + + private void CreateEndRoundButton() { int buttonHeight = (int)(GUI.Scale * 40); int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { @@ -190,12 +221,11 @@ namespace Barotrauma return true; } }; + } - campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); - CampaignUI = new CampaignUI(this, campaignUIContainer) - { - StartRound = () => { TryEndRound(); } - }; + public override void HUDScaleChanged() + { + CreateEndRoundButton(); } #endregion @@ -205,7 +235,7 @@ namespace Barotrauma base.Start(); CargoManager.CreatePurchasedItems(); UpgradeManager.ApplyUpgrades(); - UpgradeManager.SanityCheckUpgrades(Submarine.MainSub); + UpgradeManager.SanityCheckUpgrades(); if (!savedOnStart) { @@ -292,7 +322,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.UnscaledDeltaTime, textDuration); + timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); yield return CoroutineStatus.Running; } var outpost = GameMain.GameSession.Level.StartOutpost; @@ -320,7 +350,7 @@ namespace Barotrauma while (timer < fadeInDuration) { overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.UnscaledDeltaTime; + timer += CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } overlayColor = Color.Transparent; @@ -353,7 +383,7 @@ namespace Barotrauma if (prevControlled != null) { - prevControlled.SelectedConstruction = null; + prevControlled.SelectedItem = prevControlled.SelectedSecondaryItem = null; if (prevControlled.AIController != null) { prevControlled.AIController.Enabled = true; @@ -424,7 +454,7 @@ namespace Barotrauma float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { - t += CoroutineManager.UnscaledDeltaTime; + t += CoroutineManager.DeltaTime; overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); yield return CoroutineStatus.Running; } @@ -436,6 +466,7 @@ namespace Barotrauma if (success) { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + GameMain.GameSession.EventManager.RegisterEventHistory(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs deleted file mode 100644 index 76cb87ce0..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ /dev/null @@ -1,346 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.Tutorials -{ - class CaptainTutorial : ScenarioTutorial - { - // Room 1 - private float shakeTimer = 1f; - private float shakeAmount = 20f; - - // Room 2 - private Door captain_firstDoor; - private LightComponent captain_firstDoorLight; - - // Room 3 - private Character captain_medic; - private MotionSensor captain_medicObjectiveSensor; - private Vector2 captain_medicSpawnPos; - private Door tutorial_submarineDoor; - private LightComponent tutorial_submarineDoorLight; - - // Submarine - private MotionSensor captain_enteredSubmarineSensor; - private Steering captain_navConsole; - private Sonar captain_sonar; - private Item captain_statusMonitor; - private Character captain_security; - private Character captain_mechanic; - private Character captain_engineer; - private Reactor tutorial_submarineReactor; - private Door tutorial_lockedDoor_1; - private Door tutorial_lockedDoor_2; - - // Variables - private Character captain; - private LocalizedString radioSpeakerName; - private Sprite captain_steerIcon; - private Color captain_steerIconColor; - - public CaptainTutorial() : base("tutorial.captaintraining".ToIdentifier(), - new Segment( - "Captain.CommandMedic".ToIdentifier(), - "Captain.CommandMedicObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Captain.CommandMechanic".ToIdentifier(), - "Captain.CommandMechanicObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Captain.CommandMechanicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Captain.CommandSecurity".ToIdentifier(), - "Captain.CommandSecurityObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Captain.CommandSecurityText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Captain.CommandEngineer".ToIdentifier(), - "Captain.CommandEngineerObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Captain.CommandEngineerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Captain.Undock".ToIdentifier(), - "Captain.UndockObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_undock.webm", TextTag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Captain.Navigate".ToIdentifier(), - "Captain.NavigateObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_navigation.webm", TextTag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Captain.Dock".ToIdentifier(), - "Captain.DockObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_docking.webm", TextTag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80 })) - { } - - protected override CharacterInfo GetCharacterInfo() - { - return new CharacterInfo( - CharacterPrefab.HumanSpeciesName, - jobOrJobPrefab: new Job( - JobPrefab.Prefabs["captain"], Rand.RandSync.Unsynced, 0, - new Skill("medical".ToIdentifier(), 20), - new Skill("weapons".ToIdentifier(), 20), - new Skill("mechanical".ToIdentifier(), 20), - new Skill("electrical".ToIdentifier(), 20), - new Skill("helm".ToIdentifier(), 70))); - } - - protected override void Initialize() - { - captain = Character.Controlled; - radioSpeakerName = TextManager.Get("Tutorial.Radio.Watchman"); - GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - - foreach (Item item in captain.Inventory.AllItemsMod) - { - if (item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } - item.Unequip(captain); - captain.Inventory.RemoveItem(item); - } - - var steerOrder = OrderPrefab.Prefabs["steer"]; - captain_steerIcon = steerOrder.SymbolSprite; - captain_steerIconColor = steerOrder.Color; - - // Room 2 - captain_firstDoor = Item.ItemList.Find(i => i.HasTag("captain_firstdoor")).GetComponent(); - captain_firstDoorLight = Item.ItemList.Find(i => i.HasTag("captain_firstdoorlight")).GetComponent(); - - SetDoorAccess(captain_firstDoor, captain_firstDoorLight, true); - - // Room 3 - captain_medicObjectiveSensor = Item.ItemList.Find(i => i.HasTag("captain_medicobjectivesensor")).GetComponent(); - captain_medicSpawnPos = Item.ItemList.Find(i => i.HasTag("captain_medicspawnpos")).WorldPosition; - tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); - tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("medicaldoctor")) - { - TeamID = CharacterTeamType.Team1 - }; - captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); - captain_medic.GiveJobItems(null); - captain_medic.CanSpeak = captain_medic.AIController.Enabled = false; - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); - - // Submarine - captain_enteredSubmarineSensor = Item.ItemList.Find(i => i.HasTag("captain_enteredsubmarinesensor")).GetComponent(); - tutorial_submarineReactor = Item.ItemList.Find(i => i.HasTag("engineer_submarinereactor")).GetComponent(); - captain_navConsole = Item.ItemList.Find(i => i.HasTag("command")).GetComponent(); - captain_sonar = captain_navConsole.Item.GetComponent(); - captain_statusMonitor = Item.ItemList.Find(i => i.HasTag("captain_statusmonitor")); - - tutorial_submarineReactor.CanBeSelected = false; - tutorial_submarineReactor.IsActive = tutorial_submarineReactor.AutoTemp = false; - - tutorial_lockedDoor_1 = Item.ItemList.Find(i => i.HasTag("tutorial_lockeddoor_1")).GetComponent(); - tutorial_lockedDoor_2 = Item.ItemList.Find(i => i.HasTag("tutorial_lockeddoor_2")).GetComponent(); - SetDoorAccess(tutorial_lockedDoor_1, null, false); - SetDoorAccess(tutorial_lockedDoor_2, null, false); - - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("mechanic")) - { - TeamID = CharacterTeamType.Team1 - }; - captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "mechanic"); - captain_mechanic.GiveJobItems(); - - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")) - { - TeamID = CharacterTeamType.Team1 - }; - captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "securityofficer"); - captain_security.GiveJobItems(); - - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")) - { - TeamID = CharacterTeamType.Team1 - }; - captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "engineer"); - captain_engineer.GiveJobItems(); - - captain_mechanic.CanSpeak = captain_security.CanSpeak = captain_engineer.CanSpeak = false; - captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = false; - - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Started"); - GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); - } - - public override IEnumerable UpdateState() - { - while (GameMain.Instance.LoadingScreenOpen) yield return null; - - // Room 1 - while (shakeTimer > 0.0f) // Wake up, shake - { - shakeTimer -= 0.1f; - GameMain.GameScreen.Cam.Shake = shakeAmount; - yield return new WaitForSeconds(0.1f, false); - } - - // Room 2 - do { yield return null; } while (!captain_firstDoor.IsOpen); - captain_medic.AIController.Enabled = true; - - // Room 3 - do { yield return null; } while (!captain_medicObjectiveSensor.MotionDetected); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(captain_medic.Info.DisplayName, TextManager.Get("Captain.Radio.Medic"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(2f, false); - GameMain.GameSession.CrewManager.AutoShowCrewList(); - GameMain.GameSession.CrewManager.AddCharacter(captain_medic); - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); - do - { - yield return null; - // TODO: Rework order highlighting for new command UI - // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_medic, "follow", highlightColor, new Vector2(5, 5)); - } - while (!HasOrder(captain_medic, "follow")); - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - RemoveCompletedObjective(0); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective0"); - - // Submarine - do { yield return null; } while (!captain_enteredSubmarineSensor.MotionDetected); - yield return new WaitForSeconds(3f, false); - captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = true; - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); - GameMain.GameSession.CrewManager.AddCharacter(captain_mechanic); - do - { - yield return null; - // TODO: Rework order highlighting for new command UI - // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5)); - //HighlightOrderOption("jobspecific"); - } while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical")); - RemoveCompletedObjective(1); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective1"); - - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); - GameMain.GameSession.CrewManager.AddCharacter(captain_security); - do - { - yield return null; - // TODO: Rework order highlighting for new command UI - // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_security, "operateweapons", highlightColor, new Vector2(5, 5)); - HighlightOrderOption("fireatwill"); - } - while (!HasOrder(captain_security, "operateweapons")); - RemoveCompletedObjective(2); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective2"); - - yield return new WaitForSeconds(4f, false); - TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); - GameMain.GameSession.CrewManager.AddCharacter(captain_engineer); - tutorial_submarineReactor.CanBeSelected = true; - //recreate autonomous objectives to make sure the engineer didn't abandon the operate reactor objective because it was not selectable - (captain_engineer.AIController as HumanAIController).ObjectiveManager.CreateAutonomousObjectives(); - do - { - yield return null; - // TODO: Rework order highlighting for new command UI - // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_engineer, "operatereactor", highlightColor, new Vector2(5, 5)); - HighlightOrderOption("powerup"); - } - while (!HasOrder(captain_engineer, "operatereactor", "powerup")); - RemoveCompletedObjective(3); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective3"); - - do { yield return null; } while (!tutorial_submarineReactor.IsActive); // Wait until reactor on - TriggerTutorialSegment(4); - while (ContentRunning) yield return null; - captain.AddActiveObjectiveEntity(captain_navConsole.Item, captain_steerIcon, captain_steerIconColor); - SetHighlight(captain_navConsole.Item, true); - SetHighlight(captain_sonar.Item, true); - SetHighlight(captain_statusMonitor, true); - do - { - //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); - yield return new WaitForSeconds(1.0f, false); - } while (Submarine.MainSub.DockedTo.Any()); - captain_navConsole.UseAutoDocking = false; - RemoveCompletedObjective(4); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective4"); - - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(5); // Navigate to destination - do - { - if (IsSelectedItem(captain_navConsole.Item)) - { - if (captain_sonar.SonarModeSwitch.Frame.FlashTimer <= 0) - { - captain_sonar.SonarModeSwitch.Frame.Flash(highlightColor, 1.5f, false, false, new Vector2(2.5f, 2.5f)); - } - } - yield return null; - } while (captain_sonar.CurrentMode != Sonar.Mode.Active); - do { yield return null; } while (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 4000f); - RemoveCompletedObjective(5); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective5"); - - captain_navConsole.UseAutoDocking = true; - yield return new WaitForSeconds(4f, false); - TriggerTutorialSegment(6); // Docking - do - { - //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); - yield return new WaitForSeconds(1.0f, false); - } while (!Submarine.MainSub.AtEndExit || !Submarine.MainSub.DockedTo.Any()); - RemoveCompletedObjective(6); - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective6"); - - yield return new WaitForSeconds(3f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); - SetHighlight(captain_navConsole.Item, false); - SetHighlight(captain_sonar.Item, false); - SetHighlight(captain_statusMonitor, false); - captain.RemoveActiveObjectiveEntity(captain_navConsole.Item); - - GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Completed"); - CoroutineManager.StartCoroutine(TutorialCompleted()); - } - - private void HighlightOrderOption(string option) - { - if (GameMain.GameSession.CrewManager.OrderOptionButtons.Count == 0) return; - var order = GameMain.GameSession.CrewManager.OrderOptionButtons[0].UserData as Order; - - int orderIndex = 0; - for (int i = 0; i < GameMain.GameSession.CrewManager.OrderOptionButtons.Count; i++) - { - if (orderIndex >= order.Options.Length) - { - orderIndex = 0; - } - if (order.Options[orderIndex] == option) - { - if (GameMain.GameSession.CrewManager.OrderOptionButtons[i].FlashTimer <= 0) - { - GameMain.GameSession.CrewManager.OrderOptionButtons[i].Flash(highlightColor); - } - } - - orderIndex++; - } - } - - private bool IsSelectedItem(Item 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 deleted file mode 100644 index 8d9ef63b1..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ /dev/null @@ -1,521 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.Tutorials -{ - class DoctorTutorial : ScenarioTutorial - { - // Room 1 - private float shakeTimer = 1f; - private float shakeAmount = 20f; - - private LocalizedString radioSpeakerName; - private Character doctor; - - private ItemContainer doctor_suppliesCabinet; - private ItemContainer doctor_medBayCabinet; - private Character patient1, patient2; - private List subPatients; - private Hull medBay; - - private Door doctor_firstDoor; - private Door doctor_secondDoor; - private Door doctor_thirdDoor; - private Door tutorial_upperFinalDoor; - private Door tutorial_lockedDoor_2; - - private LightComponent doctor_firstDoorLight; - private LightComponent doctor_secondDoorLight; - private LightComponent doctor_thirdDoorLight; - private Door tutorial_submarineDoor; - private LightComponent tutorial_submarineDoorLight; - - // Variables - private Sprite doctor_firstAidIcon; - private Color doctor_firstAidIconColor; - - - public DoctorTutorial() : base("tutorial.medicaldoctortraining".ToIdentifier(), - new Segment( - "Doctor.Supplies".ToIdentifier(), - "Doctor.SuppliesObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Doctor.SuppliesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Doctor.OpenMedicalInterface".ToIdentifier(), - "Doctor.OpenMedicalInterfaceObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Doctor.FirstAidSelf".ToIdentifier(), - "Doctor.FirstAidSelfObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Doctor.Medbay".ToIdentifier(), - "Doctor.MedbayObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Doctor.TreatBurns".ToIdentifier(), - "Doctor.TreatBurnsObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_medinterface2.webm", TextTag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Doctor.CPR".ToIdentifier(), - "Doctor.CPRObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_cpr.webm", TextTag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Doctor.Submarine".ToIdentifier(), - "Doctor.SubmarineObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Doctor.SubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) - { } - - protected override CharacterInfo GetCharacterInfo() - { - return new CharacterInfo( - CharacterPrefab.HumanSpeciesName, - jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, - new Skill("medical".ToIdentifier(), 70), - new Skill("weapons".ToIdentifier(), 20), - new Skill("mechanical".ToIdentifier(), 20), - new Skill("electrical".ToIdentifier(), 20), - new Skill("helm".ToIdentifier(), 20))); - } - - protected override void Initialize() - { - var firstAidOrder = OrderPrefab.Prefabs["requestfirstaid"]; - doctor_firstAidIcon = firstAidOrder.SymbolSprite; - doctor_firstAidIconColor = firstAidOrder.Color; - - subPatients = new List(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); - doctor = Character.Controlled; - - foreach (Item item in doctor.Inventory.AllItemsMod) - { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } - item.Unequip(doctor); - doctor.Inventory.RemoveItem(item); - } - - doctor_suppliesCabinet = Item.ItemList.Find(i => i.HasTag("doctor_suppliescabinet"))?.GetComponent(); - doctor_medBayCabinet = Item.ItemList.Find(i => i.HasTag("doctor_medbaycabinet"))?.GetComponent(); - - var patientHull1 = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "waitingroom").CurrentHull; - var patientHull2 = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "airlock").CurrentHull; - medBay = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "medbay").CurrentHull; - - var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")) - { - TeamID = CharacterTeamType.Team1 - }; - patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); - patient1.GiveJobItems(null); - patient1.CanSpeak = false; - patient1.Params.Health.BurnReduction = 0; - patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 15.0f) }, stun: 0, playSound: false); - patient1.AIController.Enabled = false; - - assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")) - { - TeamID = CharacterTeamType.Team1 - }; - patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); - patient2.GiveJobItems(null); - patient2.CanSpeak = false; - patient2.AIController.Enabled = false; - - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")) - { - TeamID = CharacterTeamType.Team1 - }; - var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); - subPatient1.Params.Health.BurnReduction = 0; - subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); - subPatients.Add(subPatient1); - - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")); - var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); - subPatient2.TeamID = CharacterTeamType.Team1; - subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); - subPatients.Add(subPatient2); - - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")) - { - TeamID = CharacterTeamType.Team1 - }; - var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); - subPatient3.Params.Health.BurnReduction = 0; - subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); - subPatients.Add(subPatient3); - - doctor_firstDoor = Item.ItemList.Find(i => i.HasTag("doctor_firstdoor")).GetComponent(); - doctor_secondDoor = Item.ItemList.Find(i => i.HasTag("doctor_seconddoor")).GetComponent(); - doctor_thirdDoor = Item.ItemList.Find(i => i.HasTag("doctor_thirddoor")).GetComponent(); - tutorial_upperFinalDoor = Item.ItemList.Find(i => i.HasTag("tutorial_upperfinaldoor")).GetComponent(); - doctor_firstDoorLight = Item.ItemList.Find(i => i.HasTag("doctor_firstdoorlight")).GetComponent(); - doctor_secondDoorLight = Item.ItemList.Find(i => i.HasTag("doctor_seconddoorlight")).GetComponent(); - doctor_thirdDoorLight = Item.ItemList.Find(i => i.HasTag("doctor_thirddoorlight")).GetComponent(); - SetDoorAccess(doctor_firstDoor, doctor_firstDoorLight, false); - SetDoorAccess(doctor_secondDoor, doctor_secondDoorLight, false); - SetDoorAccess(doctor_thirdDoor, doctor_thirdDoorLight, false); - tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); - tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); - tutorial_lockedDoor_2 = Item.ItemList.Find(i => i.HasTag("tutorial_lockeddoor_2")).GetComponent(); - SetDoorAccess(tutorial_lockedDoor_2, null, true); - - - foreach (var patient in subPatients) - { - patient.CanSpeak = false; - patient.AIController.Enabled = false; - patient.GiveJobItems(); - } - - Item reactorItem = Item.ItemList.Find(i => i.Submarine == Submarine.MainSub && i.GetComponent() != null); - reactorItem.GetComponent().AutoTemp = true; - - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Started"); - GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); - } - - public override IEnumerable UpdateState() - { - while (GameMain.Instance.LoadingScreenOpen) yield return null; - - // explosions and radio messages ------------------------------------------------------ - - yield return new WaitForSeconds(3.0f, false); - - //SoundPlayer.PlayDamageSound("StructureBlunt", 10, Character.Controlled.WorldPosition); - //// Room 1 - //while (shakeTimer > 0.0f) // Wake up, shake - //{ - // shakeTimer -= 0.1f; - // GameMain.GameScreen.Cam.Shake = shakeAmount; - // yield return new WaitForSeconds(0.1f); - //} - //yield return new WaitForSeconds(2.5f); - //GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.WakeUp"), ChatMessageType.Radio, null); - - //yield return new WaitForSeconds(2.5f); - - doctor.SetStun(1.5f); - 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); - SoundPlayer.PlayDamageSound("StructureBlunt", 10, Character.Controlled.WorldPosition - Vector2.UnitX * 25); - - yield return new WaitForSeconds(0.5f, false); - - doctor.DamageLimb( - Character.Controlled.WorldPosition, - doctor.AnimController.GetLimb(LimbType.Torso), - new List { new Affliction(AfflictionPrefab.InternalDamage, 10.0f) }, - stun: 3.0f, playSound: true, attackImpulse: 0.0f); - - shakeTimer = 0.5f; - while (shakeTimer > 0.0f) // Wake up, shake - { - shakeTimer -= 0.1f; - GameMain.GameScreen.Cam.Shake = shakeAmount; - yield return new WaitForSeconds(0.1f, false); - } - - yield return new WaitForSeconds(3.0f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.KnockedDown"), ChatMessageType.Radio, null); - - // first tutorial segment, get medical supplies ------------------------------------------------------ - - yield return new WaitForSeconds(1.5f, false); - SetHighlight(doctor_suppliesCabinet.Item, true); - - /*while (doctor.CurrentHull != doctor_suppliesCabinet.Item.CurrentHull) - { - yield return new WaitForSeconds(2.0f); - }*/ - - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), "None"); // Medical supplies objective - - do - { - for (int i = 0; i < doctor_suppliesCabinet.Inventory.Capacity; i++) - { - if (doctor_suppliesCabinet.Inventory.GetItemAt(i) != null) - { - HighlightInventorySlot(doctor_suppliesCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } - if (doctor.SelectedConstruction == doctor_suppliesCabinet.Item) - { - for (int i = 0; i < doctor.Inventory.Capacity; i++) - { - if (doctor.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antidama1".ToIdentifier()) == null); // Wait until looted - yield return new WaitForSeconds(1.0f, false); - - SetHighlight(doctor_suppliesCabinet.Item, false); - RemoveCompletedObjective(0); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective0"); - - yield return new WaitForSeconds(1.0f, false); - - // 2nd tutorial segment, treat self ------------------------------------------------------------------------- - - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // Open health interface - while (CharacterHealth.OpenHealthWindow == null) - { - doctor.CharacterHealth.HealthBarPulsateTimer = 1.0f; - yield return null; - } - yield return null; - RemoveCompletedObjective(1); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective1"); - yield return new WaitForSeconds(1.0f, false); - TriggerTutorialSegment(2); //Treat self - while (doctor.CharacterHealth.GetAfflictionStrength("damage") > 0.01f) - { - if (CharacterHealth.OpenHealthWindow == null) - { - doctor.CharacterHealth.HealthBarPulsateTimer = 1.0f; - } - else - { - HighlightInventorySlot(doctor.Inventory, "antidama1".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - - yield return null; - } - - RemoveCompletedObjective(2); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective2"); - SetDoorAccess(doctor_firstDoor, doctor_firstDoorLight, true); - - while (CharacterHealth.OpenHealthWindow != null) - { - yield return new WaitForSeconds(1.0f, false); - } - - // treat patient -------------------------------------------------------------------------------------------- - - //patient 1 requests first aid - var newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient1.CurrentHull, null, orderGiver: patient1); - doctor.AddActiveObjectiveEntity(patient1, doctor_firstAidIcon, doctor_firstAidIconColor); - //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient1.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); - - while (doctor.CurrentHull != patient1.CurrentHull) - { - yield return new WaitForSeconds(1.0f, false); - } - yield return new WaitForSeconds(0.0f, false); - - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.AssistantBurns"), ChatMessageType.Radio, null); - GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - GameMain.GameSession.CrewManager.AddCharacter(doctor); - GameMain.GameSession.CrewManager.AddCharacter(patient1); - GameMain.GameSession.CrewManager.AutoShowCrewList(); - patient1.CharacterHealth.UseHealthWindow = false; - - yield return new WaitForSeconds(3.0f, false); - patient1.AIController.Enabled = true; - doctor.RemoveActiveObjectiveEntity(patient1); - TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); // Get the patient to medbay - - while (patient1.GetCurrentOrderWithTopPriority()?.Identifier != "follow") - { - // TODO: Rework order highlighting for new command UI - // GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); - yield return null; - } - - SetDoorAccess(doctor_secondDoor, doctor_secondDoorLight, true); - - while (patient1.CurrentHull != medBay) - { - yield return new WaitForSeconds(1.0f, false); - } - RemoveCompletedObjective(3); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective3"); - SetHighlight(doctor_medBayCabinet.Item, true); - SetDoorAccess(doctor_thirdDoor, doctor_thirdDoorLight, true); - patient1.CharacterHealth.UseHealthWindow = true; - - yield return new WaitForSeconds(2.0f, false); - - TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // treat burns - - do - { - for (int i = 0; i < 3; i++) - { - if (doctor_medBayCabinet.Inventory.GetItemAt(i) != null) - { - HighlightInventorySlot(doctor_medBayCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } - if (doctor.SelectedConstruction == doctor_medBayCabinet.Item) - { - for (int i = 0; i < doctor.Inventory.Capacity; i++) - { - if (doctor.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antibleeding1".ToIdentifier()) == null); // Wait until looted - SetHighlight(doctor_medBayCabinet.Item, false); - SetHighlight(patient1, true); - - while (patient1.CharacterHealth.GetAfflictionStrength("burn") > 0.01f) - { - if (CharacterHealth.OpenHealthWindow == null) - { - doctor.CharacterHealth.HealthBarPulsateTimer = 1.0f; - } - else - { - HighlightInventorySlot(doctor.Inventory, "antibleeding1".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - yield return null; - - } - RemoveCompletedObjective(4); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective4"); - SetHighlight(patient1, false); - yield return new WaitForSeconds(1.0f, false); - - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.AssistantBurnsHealed"), ChatMessageType.Radio, null); - - // treat unconscious patient ------------------------------------------------------ - - //patient calls for help - //patient2.CanSpeak = true; - yield return new WaitForSeconds(2.0f, false); - newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient2.CurrentHull, null, orderGiver: patient2); - doctor.AddActiveObjectiveEntity(patient2, doctor_firstAidIcon, doctor_firstAidIconColor); - //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient2.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); - patient2.AIController.Enabled = true; - patient2.Oxygen = -50; - CoroutineManager.StartCoroutine(KeepPatientAlive(patient2), "KeepPatient2Alive"); - - /*while (doctor.CurrentHull != patient2.CurrentHull) - { - yield return new WaitForSeconds(1.0f); - }*/ - do { yield return null; } while (!tutorial_upperFinalDoor.IsOpen); - yield return new WaitForSeconds(2.0f, false); - - TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // perform CPR - SetHighlight(patient2, true); - while (patient2.IsUnconscious) - { - if (CharacterHealth.OpenHealthWindow != null && doctor.AnimController.Anim != AnimController.Animation.CPR) - { - //Disabled pulse until it's replaced by a better effect - //CharacterHealth.OpenHealthWindow.CPRButton.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.0f); - if (CharacterHealth.OpenHealthWindow.CPRButton.FlashTimer <= 0.0f) - { - CharacterHealth.OpenHealthWindow.CPRButton.Flash(highlightColor); - } - } - yield return null; - } - RemoveCompletedObjective(5); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective5"); - SetHighlight(patient2, false); - doctor.RemoveActiveObjectiveEntity(patient2); - CoroutineManager.StopCoroutines("KeepPatient2Alive"); - - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - - while (doctor.Submarine != Submarine.MainSub) - { - yield return new WaitForSeconds(1.0f, false); - } - - subPatients[2].Oxygen = -50; - CoroutineManager.StartCoroutine(KeepPatientAlive(subPatients[2]), "KeepPatient3Alive"); - - yield return new WaitForSeconds(5.0f, false); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.EnteredSub"), ChatMessageType.Radio, null); - - yield return new WaitForSeconds(3.0f, false); - TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // give treatment to anyone in need - - foreach (var patient in subPatients) - { - //patient.CanSpeak = true; - patient.AIController.Enabled = true; - SetHighlight(patient, true); - } - - double subEnterTime = Timing.TotalTime; - - bool[] patientCalledHelp = new bool[] { false, false, false }; - - while (subPatients.Any(p => p.Vitality < p.MaxVitality * 0.9f && !p.IsDead)) - { - for (int i = 0; i < subPatients.Count; i++) - { - //make patients call for help to make sure the player finds them - //(within 1 minute intervals of entering the sub) - if (!patientCalledHelp[i] && Timing.TotalTime > subEnterTime + 60 * (i + 1)) - { - doctor.AddActiveObjectiveEntity(subPatients[i], doctor_firstAidIcon, doctor_firstAidIconColor); - newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); - string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName?.Value, givingOrderToSelf: false); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(subPatients[i].Name, message, ChatMessageType.Order, null); - patientCalledHelp[i] = true; - } - - if (subPatients[i].ExternalHighlight && subPatients[i].Vitality >= subPatients[i].MaxVitality * 0.9f) - { - doctor.RemoveActiveObjectiveEntity(subPatients[i]); - SetHighlight(subPatients[i], false); - } - } - yield return new WaitForSeconds(1.0f, false); - } - RemoveCompletedObjective(6); - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective6"); - foreach (var patient in subPatients) - { - SetHighlight(patient, false); - doctor.RemoveActiveObjectiveEntity(patient); - } - - // END TUTORIAL - GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Completed"); - CoroutineManager.StartCoroutine(TutorialCompleted()); - } - - public IEnumerable KeepPatientAlive(Character patient) - { - while (patient != null && !patient.Removed) - { - patient.Oxygen = Math.Max(patient.Oxygen, -50); - yield return null; - } - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs deleted file mode 100644 index b95fa6f31..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ /dev/null @@ -1,664 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; - -namespace Barotrauma.Tutorials -{ - class EngineerTutorial : ScenarioTutorial - { - // Other tutorial items - private LightComponent tutorial_securityFinalDoorLight; - private LightComponent tutorial_mechanicFinalDoorLight; - private Steering tutorial_submarineSteering; - - // Room 1 - private float shakeTimer = 1f; - private float shakeAmount = 20f; - - // Room 2 - private MotionSensor engineer_equipmentObjectiveSensor; - private ItemContainer engineer_equipmentCabinet; - private Door engineer_firstDoor; - private LightComponent engineer_firstDoorLight; - - // Room 3 - private Powered tutorial_oxygenGenerator; - private Reactor engineer_reactor; - private Door engineer_secondDoor; - private LightComponent engineer_secondDoorLight; - - // Room 4 - private Item engineer_brokenJunctionBox; - private Door engineer_thirdDoor; - private LightComponent engineer_thirdDoorLight; - - // Room 5 - private PowerTransfer[] engineer_disconnectedJunctionBoxes; - private ConnectionPanel[] engineer_disconnectedConnectionPanels; - private Item engineer_wire_1; - private Powered engineer_lamp_1; - private Item engineer_wire_2; - private Powered engineer_lamp_2; - private Door engineer_fourthDoor; - private LightComponent engineer_fourthDoorLight; - - // Room 6 - private Pump engineer_workingPump; - private Door tutorial_lockedDoor_1; - - // Submarine - private Door tutorial_submarineDoor; - private LightComponent tutorial_submarineDoorLight; - private MotionSensor tutorial_enteredSubmarineSensor; - private Item engineer_submarineJunctionBox_1; - private Item engineer_submarineJunctionBox_2; - private Item engineer_submarineJunctionBox_3; - private Reactor engineer_submarineReactor; - - // Variables - private LocalizedString radioSpeakerName; - private Character engineer; - private int[] reactorLoads = new int[5] { 1500, 3000, 2000, 5000, 3500 }; - private float reactorLoadChangeTime = 2f; - private float reactorLoadError = 200f; - private bool reactorOperatedProperly; - private const float waterVolumeBeforeOpening = 15f; - private Sprite engineer_repairIcon; - private Color engineer_repairIconColor; - private Sprite engineer_reactorIcon; - private Color engineer_reactorIconColor; - private bool wiringActive = false; - - public EngineerTutorial() : base("tutorial.engineertraining".ToIdentifier(), - new Segment( - "Mechanic.Equipment".ToIdentifier(), - "Mechanic.EquipmentObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Engineer.Reactor".ToIdentifier(), - "Engineer.ReactorObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), - new Segment( - "Engineer.OperateReactor".ToIdentifier(), - "Engineer.OperateReactorObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Engineer.OperateReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), - new Segment( - "Engineer.RepairJunctionBox".ToIdentifier(), - "Engineer.RepairJunctionBoxObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Engineer.RepairJunctionBoxText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Engineer.WireJunctionBoxes".ToIdentifier(), - "Engineer.WireJunctionBoxesObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_wiring.webm", TextTag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Engineer.RepairElectricalRoom".ToIdentifier(), - "Engineer.RepairElectricalRoomObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Engineer.RepairElectricalRoomText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Engineer.PowerUpReactor".ToIdentifier(), - "Engineer.PowerUpReactorObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Engineer.PowerUpReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center })) - { } - - protected override CharacterInfo GetCharacterInfo() - { - return new CharacterInfo( - CharacterPrefab.HumanSpeciesName, - jobOrJobPrefab: new Job( - JobPrefab.Prefabs["engineer"], Rand.RandSync.Unsynced, 0, - new Skill("medical".ToIdentifier(), 0), - new Skill("weapons".ToIdentifier(), 0), - new Skill("mechanical".ToIdentifier(), 20), - new Skill("electrical".ToIdentifier(), 60), - new Skill("helm".ToIdentifier(), 0))); - } - - protected override void Initialize() - { - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); - engineer = Character.Controlled; - - foreach (Item item in engineer.Inventory.AllItemsMod) - { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } - item.Unequip(engineer); - engineer.Inventory.RemoveItem(item); - } - - var repairOrder = OrderPrefab.Prefabs["repairsystems"]; - engineer_repairIcon = repairOrder.SymbolSprite; - engineer_repairIconColor = repairOrder.Color; - - var reactorOrder = OrderPrefab.Prefabs["operatereactor"]; - engineer_reactorIcon = reactorOrder.SymbolSprite; - engineer_reactorIconColor = reactorOrder.Color; - - // Other tutorial items - tutorial_securityFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_securityfinaldoorlight")).GetComponent(); - tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); - tutorial_submarineSteering = Item.ItemList.Find(i => i.HasTag("command")).GetComponent(); - - tutorial_submarineSteering.CanBeSelected = false; - foreach (ItemComponent ic in tutorial_submarineSteering.Item.Components) - { - ic.CanBeSelected = false; - } - - SetDoorAccess(null, tutorial_securityFinalDoorLight, false); - SetDoorAccess(null, tutorial_mechanicFinalDoorLight, false); - - // Room 2 - engineer_equipmentObjectiveSensor = Item.ItemList.Find(i => i.HasTag("engineer_equipmentobjectivesensor")).GetComponent(); - engineer_equipmentCabinet = Item.ItemList.Find(i => i.HasTag("engineer_equipmentcabinet")).GetComponent(); - engineer_firstDoor = Item.ItemList.Find(i => i.HasTag("engineer_firstdoor")).GetComponent(); - engineer_firstDoorLight = Item.ItemList.Find(i => i.HasTag("engineer_firstdoorlight")).GetComponent(); - - SetDoorAccess(engineer_firstDoor, engineer_firstDoorLight, false); - - // Room 3 - tutorial_oxygenGenerator = Item.ItemList.Find(i => i.HasTag("tutorial_oxygengenerator")).GetComponent(); - engineer_reactor = Item.ItemList.Find(i => i.HasTag("engineer_reactor")).GetComponent(); - engineer_reactor.FireDelay = engineer_reactor.MeltdownDelay = float.PositiveInfinity; - engineer_reactor.FuelConsumptionRate = 0.0f; - engineer_reactor.PowerOn = true; - reactorOperatedProperly = false; - - engineer_secondDoor = Item.ItemList.Find(i => i.HasTag("engineer_seconddoor")).GetComponent(); ; - engineer_secondDoorLight = Item.ItemList.Find(i => i.HasTag("engineer_seconddoorlight")).GetComponent(); - - SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, false); - - // Room 4 - engineer_brokenJunctionBox = Item.ItemList.Find(i => i.HasTag("engineer_brokenjunctionbox")); - engineer_thirdDoor = Item.ItemList.Find(i => i.HasTag("engineer_thirddoor")).GetComponent(); - engineer_thirdDoorLight = Item.ItemList.Find(i => i.HasTag("engineer_thirddoorlight")).GetComponent(); - - engineer_brokenJunctionBox.Indestructible = false; - engineer_brokenJunctionBox.Condition = 0f; - - SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, false); - - // Room 5 - engineer_disconnectedJunctionBoxes = new PowerTransfer[4]; - engineer_disconnectedConnectionPanels = new ConnectionPanel[4]; - - for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - { - engineer_disconnectedJunctionBoxes[i] = Item.ItemList.Find(item => item.HasTag($"engineer_disconnectedjunctionbox_{i + 1}")).GetComponent(); - engineer_disconnectedConnectionPanels[i] = engineer_disconnectedJunctionBoxes[i].Item.GetComponent(); - engineer_disconnectedConnectionPanels[i].Locked = false; - - for (int j = 0; j < engineer_disconnectedJunctionBoxes[i].PowerConnections.Count; j++) - { - foreach (Wire wire in engineer_disconnectedJunctionBoxes[i].PowerConnections[j].Wires) - { - if (wire == null) continue; - wire.Locked = true; - } - } - } - - engineer_wire_1 = Item.ItemList.Find(i => i.HasTag("engineer_wire_1")); - engineer_wire_1.SpriteColor = Color.Transparent; - engineer_wire_2 = Item.ItemList.Find(i => i.HasTag("engineer_wire_2")); - engineer_wire_2.SpriteColor = Color.Transparent; - engineer_lamp_1 = Item.ItemList.Find(i => i.HasTag("engineer_lamp_1")).GetComponent(); - engineer_lamp_2 = Item.ItemList.Find(i => i.HasTag("engineer_lamp_2")).GetComponent(); - engineer_fourthDoor = Item.ItemList.Find(i => i.HasTag("engineer_fourthdoor")).GetComponent(); - engineer_fourthDoorLight = Item.ItemList.Find(i => i.HasTag("engineer_fourthdoorlight")).GetComponent(); - SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, false); - - // Room 6 - engineer_workingPump = Item.ItemList.Find(i => i.HasTag("engineer_workingpump")).GetComponent(); - engineer_workingPump.Item.CurrentHull.WaterVolume += engineer_workingPump.Item.CurrentHull.Volume; - engineer_workingPump.IsActive = true; - tutorial_lockedDoor_1 = Item.ItemList.Find(i => i.HasTag("tutorial_lockeddoor_1")).GetComponent(); - SetDoorAccess(tutorial_lockedDoor_1, null, true); - - // Submarine - tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); - tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - - tutorial_enteredSubmarineSensor = Item.ItemList.Find(i => i.HasTag("tutorial_enteredsubmarinesensor")).GetComponent(); - engineer_submarineJunctionBox_1 = Item.ItemList.Find(i => i.HasTag("engineer_submarinejunctionbox_1")); - engineer_submarineJunctionBox_2 = Item.ItemList.Find(i => i.HasTag("engineer_submarinejunctionbox_2")); - engineer_submarineJunctionBox_3 = Item.ItemList.Find(i => i.HasTag("engineer_submarinejunctionbox_3")); - engineer_submarineReactor = Item.ItemList.Find(i => i.HasTag("engineer_submarinereactor")).GetComponent(); - engineer_submarineReactor.PowerOn = true; - engineer_submarineReactor.IsActive = engineer_submarineReactor.AutoTemp = false; - - engineer_submarineJunctionBox_1.Indestructible = false; - engineer_submarineJunctionBox_1.Condition = 0f; - engineer_submarineJunctionBox_2.Indestructible = false; - engineer_submarineJunctionBox_2.Condition = 0f; - engineer_submarineJunctionBox_3.Indestructible = false; - engineer_submarineJunctionBox_3.Condition = 0f; - - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Started"); - GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); - } - - public override IEnumerable UpdateState() - { - while (GameMain.Instance.LoadingScreenOpen) { yield return null; } - - // Room 1 - SoundPlayer.PlayDamageSound("StructureBlunt", 10, Character.Controlled.WorldPosition); - while (shakeTimer > 0.0f) // Wake up, shake - { - shakeTimer -= 0.1f; - GameMain.GameScreen.Cam.Shake = shakeAmount; - yield return new WaitForSeconds(0.1f, false); - } - - //// Remove - //for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - //{ - // SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, true); - //} - //do { CheckGhostWires(); HandleJunctionBoxWiringHighlights(); yield return null; } while (engineer_workingPump.Voltage < engineer_workingPump.MinVoltage); // Wait until connected all the way to the pump - //CheckGhostWires(); - //// Remove - - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.WakeUp"), ChatMessageType.Radio, null); - SetHighlight(engineer_equipmentCabinet.Item, true); - - // Room 2 - do { yield return null; } while (!engineer_equipmentObjectiveSensor.MotionDetected); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Equipment"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(0.5f, false); - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Retrieve equipment - bool firstSlotRemoved = false; - bool secondSlotRemoved = false; - bool thirdSlotRemoved = false; - bool fourthSlotRemoved = false; - do - { - if (IsSelectedItem(engineer_equipmentCabinet.Item)) - { - if (!firstSlotRemoved) - { - HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } - } - - if (!secondSlotRemoved) - { - HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } - } - - if (!thirdSlotRemoved) - { - HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } - } - - if (!fourthSlotRemoved) - { - HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 3, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.GetItemAt(2) == null) { fourthSlotRemoved = true; } - } - - for (int i = 0; i < engineer.Inventory.visualSlots.Length; i++) - { - if (engineer.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(engineer.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - - yield return null; - } while (!engineer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted - RemoveCompletedObjective(0); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective0"); - SetHighlight(engineer_equipmentCabinet.Item, false); - SetHighlight(engineer_reactor.Item, true); - SetDoorAccess(engineer_firstDoor, engineer_firstDoorLight, true); - - // Room 3 - do { yield return null; } while (!IsSelectedItem(engineer_reactor.Item)); - yield return new WaitForSeconds(0.5f, false); - TriggerTutorialSegment(1); - do - { - if (IsSelectedItem(engineer_reactor.Item)) - { - engineer_reactor.AutoTemp = false; - if (engineer_reactor.PowerButton.FlashTimer <= 0) - { - engineer_reactor.PowerButton.Flash(highlightColor, 1.5f, false); - } - } - yield return null; - } while (!engineer_reactor.PowerOn); - do - { - if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.visualSlots != null) - { - engineer_reactor.AutoTemp = false; - HighlightInventorySlot(engineer.Inventory, "fuelrod".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - - for (int i = 0; i < engineer_reactor.Item.OwnInventory.visualSlots.Length; i++) - { - HighlightInventorySlot(engineer_reactor.Item.OwnInventory, i, highlightColor, 0.5f, 0.5f, 0f); - } - } - yield return null; - } while (engineer_reactor.AvailableFuel == 0); - RemoveCompletedObjective(1); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective1"); - TriggerTutorialSegment(2); - CoroutineManager.StartCoroutine(ReactorOperatedProperly()); - do - { - if (IsSelectedItem(engineer_reactor.Item)) - { - engineer_reactor.AutoTemp = false; - if (engineer_reactor.FissionRateScrollBar.FlashTimer <= 0) - { - engineer_reactor.FissionRateScrollBar.Flash(highlightColor, 1.5f); - } - - if (engineer_reactor.TurbineOutputScrollBar.FlashTimer <= 0) - { - engineer_reactor.TurbineOutputScrollBar.Flash(highlightColor, 1.5f); - } - } - yield return null; - } while (!reactorOperatedProperly); - yield return new WaitForSeconds(2f, false); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.ReactorStable"), ChatMessageType.Radio, null); - do - { - if (IsSelectedItem(engineer_reactor.Item)) - { - if (engineer_reactor.AutoTempSwitch.FlashTimer <= 0) - { - engineer_reactor.AutoTempSwitch.Flash(highlightColor, 1.5f, false, false, new Vector2(10, 10)); - } - } - yield return null; - } while (!engineer_reactor.AutoTemp); - - float wait = 1.5f; - do - { - yield return new WaitForSeconds(0.1f, false); - wait -= 0.1f; - engineer_reactor.AutoTemp = true; - } while (wait > 0.0f); - engineer.SelectedConstruction = null; - engineer_reactor.CanBeSelected = false; - RemoveCompletedObjective(2); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective2"); - SetHighlight(engineer_reactor.Item, false); - SetHighlight(engineer_brokenJunctionBox, true); - SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, true); - - // Room 4 - do { yield return null; } while (!engineer_secondDoor.IsOpen); - yield return new WaitForSeconds(1f, false); - Repairable repairableJunctionBoxComponent = engineer_brokenJunctionBox.GetComponent(); - TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Repair the junction box - do - { - if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) - { - HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - else if (IsSelectedItem(engineer_brokenJunctionBox) && repairableJunctionBoxComponent.CurrentFixer == null) - { - if (repairableJunctionBoxComponent.RepairButton.FlashTimer <= 0) - { - repairableJunctionBoxComponent.RepairButton.Flash(); - } - } - yield return null; - } while (repairableJunctionBoxComponent.IsBelowRepairThreshold); // Wait until repaired - SetHighlight(engineer_brokenJunctionBox, false); - RemoveCompletedObjective(3); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective3"); - SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); - for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - { - SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, true); - } - - // Room 5 - do { yield return null; } while (!engineer_thirdDoor.IsOpen); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.FaultyWiring"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Connect the junction boxes - do { CheckGhostWires(); HandleJunctionBoxWiringHighlights(); yield return null; } while (engineer_workingPump.Voltage < engineer_workingPump.MinVoltage); // Wait until connected all the way to the pump - CheckGhostWires(); - for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - { - SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false); - } - RemoveCompletedObjective(4); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective4"); - do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained - wiringActive = false; - SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.ChangeOfPlans"), ChatMessageType.Radio, null); - - // Submarine - do { yield return null; } while (!tutorial_enteredSubmarineSensor.MotionDetected); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Submarine"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(5); // Repair junction box - while (ContentRunning) yield return null; - engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_1, engineer_repairIcon, engineer_repairIconColor); - engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_2, engineer_repairIcon, engineer_repairIconColor); - engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_3, engineer_repairIcon, engineer_repairIconColor); - SetHighlight(engineer_submarineJunctionBox_1, true); - SetHighlight(engineer_submarineJunctionBox_2, true); - SetHighlight(engineer_submarineJunctionBox_3, true); - - Repairable repairableJunctionBoxComponent1 = engineer_submarineJunctionBox_1.GetComponent(); - Repairable repairableJunctionBoxComponent2 = engineer_submarineJunctionBox_2.GetComponent(); - Repairable repairableJunctionBoxComponent3 = engineer_submarineJunctionBox_3.GetComponent(); - - // Remove highlights when each individual machine is repaired - do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (repairableJunctionBoxComponent1.IsBelowRepairThreshold || repairableJunctionBoxComponent2.IsBelowRepairThreshold || repairableJunctionBoxComponent3.IsBelowRepairThreshold); - CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); - RemoveCompletedObjective(5); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective5"); - yield return new WaitForSeconds(2f, false); - - TriggerTutorialSegment(6); // Powerup reactor - SetHighlight(engineer_submarineReactor.Item, true); - engineer.AddActiveObjectiveEntity(engineer_submarineReactor.Item, engineer_reactorIcon, engineer_reactorIconColor); - do { yield return null; } while (!IsReactorPoweredUp(engineer_submarineReactor)); // Wait until ~matches load - engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item); - SetHighlight(engineer_submarineReactor.Item, false); - RemoveCompletedObjective(6); - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective6"); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null); - - yield return new WaitForSeconds(4f, false); - - GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Completed"); - CoroutineManager.StartCoroutine(TutorialCompleted()); - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if (wiringActive) - { - for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - { - for (int j = 0; j < engineer_disconnectedJunctionBoxes[i].PowerConnections.Count; j++) - { - engineer_disconnectedJunctionBoxes[i].PowerConnections[j].UpdateFlashTimer(deltaTime); - } - } - } - } - - private bool IsSelectedItem(Item item) - { - return engineer?.SelectedConstruction == item; - } - - private IEnumerable ReactorOperatedProperly() - { - float timer; - - for (int i = 0; i < reactorLoads.Length; i++) - { - timer = reactorLoadChangeTime; - tutorial_oxygenGenerator.PowerConsumption = reactorLoads[i]; - while (timer > 0) - { - yield return CoroutineStatus.Running; - if (CoroutineManager.DeltaTime > 0.0f && IsReactorPoweredUp(engineer_reactor)) - { - timer -= CoroutineManager.DeltaTime; - } - } - } - - reactorOperatedProperly = true; - } - - private void CheckGhostWires() - { - Color wireColor = - Color.Orange * - MathHelper.Lerp(0.25f, 0.75f, (float)(Math.Sin((Timing.TotalTime * 4.0f)) + 1.0f) / 2.0f); - - if (engineer_wire_1 != null) - { - engineer_wire_1.SpriteColor = wireColor; - if (engineer_lamp_1.Voltage > engineer_lamp_1.MinVoltage) - { - engineer_wire_1.Remove(); - engineer_wire_1 = null; - } - } - - - if (engineer_wire_2 != null) - { - engineer_wire_2.SpriteColor = wireColor; - if (engineer_lamp_2.Voltage > engineer_lamp_2.MinVoltage) - { - engineer_wire_2.Remove(); - engineer_wire_2 = null; - } - } - - } - - private void HandleJunctionBoxWiringHighlights() - { - Item selected = engineer.SelectedConstruction; - - if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) - { - HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - } - - int selectedIndex = -1; - - if (selected != null) - { - for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) - { - if (selected == engineer_disconnectedJunctionBoxes[i].Item) - { - selectedIndex = i; - break; - } - } - } - - wiringActive = selectedIndex != -1; - - if (!engineer.HasEquippedItem("wire".ToIdentifier())) - { - HighlightInventorySlotWithTag(engineer.Inventory, "wire".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - } - else - { - if (!wiringActive) return; - for (int i = 0; i < engineer_disconnectedConnectionPanels[selectedIndex].Connections.Count; i++) - { - var connection = engineer_disconnectedConnectionPanels[selectedIndex].Connections[i]; - if (connection.IsPower && connection.FlashTimer <= 0) - { - foreach (Wire wire in engineer_disconnectedConnectionPanels[selectedIndex].Connections[i].Wires) - { - if (wire == null) continue; - if (!wire.Locked) - { - return; - } - } - - connection.Flash(highlightColor); - } - } - } - } - - private void CheckJunctionBoxHighlights(Repairable comp1, Repairable comp2, Repairable comp3) - { - if (!comp1.IsBelowRepairThreshold && engineer_submarineJunctionBox_1.ExternalHighlight) - { - SetHighlight(engineer_submarineJunctionBox_1, false); - engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_1); - } - if (!comp2.IsBelowRepairThreshold && engineer_submarineJunctionBox_2.ExternalHighlight) - { - SetHighlight(engineer_submarineJunctionBox_2, false); - engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_2); - } - if (!comp3.IsBelowRepairThreshold && engineer_submarineJunctionBox_3.ExternalHighlight) - { - SetHighlight(engineer_submarineJunctionBox_3, false); - engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_3); - } - } - - private bool IsReactorPoweredUp(Reactor reactor) - { - float load = 0.0f; - List connections = reactor.Item.Connections; - if (connections != null && connections.Count > 0) - { - foreach (Connection connection in connections) - { - if (!connection.IsPower) continue; - foreach (Connection recipient in connection.Recipients) - { - if (!(recipient.Item is Item it)) continue; - - PowerTransfer pt = it.GetComponent(); - if (pt == null) continue; - - load = Math.Max(load, pt.PowerLoad); - } - } - } - - return Math.Abs(load + reactor.CurrPowerConsumption) < reactorLoadError; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs deleted file mode 100644 index 06d05b708..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ /dev/null @@ -1,737 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; - -namespace Barotrauma.Tutorials -{ - class MechanicTutorial : ScenarioTutorial - { - // Other tutorial items - private LightComponent tutorial_securityFinalDoorLight; - private Door tutorial_upperFinalDoor; - private Steering tutorial_submarineSteering; - - // Room 1 - private float shakeTimer = 1f; - private float shakeAmount = 20f; - private Door mechanic_firstDoor; - private LightComponent mechanic_firstDoorLight; - - // Room 2 - private MotionSensor mechanic_equipmentObjectiveSensor; - private ItemContainer mechanic_equipmentCabinet; - private Door mechanic_secondDoor; - private LightComponent mechanic_secondDoorLight; - - // Room 3 - private MotionSensor mechanic_weldingObjectiveSensor; - private Pump mechanic_workingPump; - private Door mechanic_thirdDoor; - private LightComponent mechanic_thirdDoorLight; - private Structure mechanic_brokenWall_1; - private Hull mechanic_brokenhull_1; - - // Room 4 - private MotionSensor mechanic_craftingObjectiveSensor; - private Deconstructor mechanic_deconstructor; - private Fabricator mechanic_fabricator; - private ItemContainer mechanic_craftingCabinet; - private Door mechanic_fourthDoor; - private LightComponent mechanic_fourthDoorLight; - - // Room 5 - private MotionSensor mechanic_fireSensor; - private DummyFireSource mechanic_fire; - private Door mechanic_fifthDoor; - private LightComponent mechanic_fifthDoorLight; - - // Room 6 - private MotionSensor mechanic_divingSuitObjectiveSensor; - private ItemContainer mechanic_divingSuitContainer; - private ItemContainer mechanic_oxygenContainer; - private Door tutorial_mechanicFinalDoor; - private LightComponent tutorial_mechanicFinalDoorLight; - - // Room 7 - private Pump mechanic_brokenPump; - private Structure mechanic_brokenWall_2; - private Hull mechanic_brokenhull_2; - private Door tutorial_submarineDoor; - private LightComponent tutorial_submarineDoorLight; - - // Submarine - private MotionSensor tutorial_enteredSubmarineSensor; - private Engine mechanic_submarineEngine; - private Pump mechanic_ballastPump_1; - private Pump mechanic_ballastPump_2; - - // Variables - private const float waterVolumeBeforeOpening = 15f; - private LocalizedString radioSpeakerName; - private Character mechanic; - private Sprite mechanic_repairIcon; - private Color mechanic_repairIconColor; - private Sprite mechanic_weldIcon; - - public MechanicTutorial() : base("tutorial.mechanictraining".ToIdentifier(), - new Segment( - "Mechanic.OpenDoor".ToIdentifier(), - "Mechanic.OpenDoorObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.OpenDoorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.Equipment".ToIdentifier(), - "Mechanic.EquipmentObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_inventory.webm", TextTag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Mechanic.Welding".ToIdentifier(), - "Mechanic.WeldingObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_equip.webm", TextTag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Mechanic.Drain".ToIdentifier(), - "Mechanic.DrainObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.DrainText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.Deconstruct".ToIdentifier(), - "Mechanic.DeconstructObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_deconstruct.webm", TextTag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Mechanic.Fabricate".ToIdentifier(), - "Mechanic.FabricateObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_fabricate.webm", TextTag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Mechanic.Extinguisher".ToIdentifier(), - "Mechanic.ExtinguisherObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.ExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.DropExtinguisher".ToIdentifier(), - "Mechanic.DropExtinguisherObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.DropExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.Diving".ToIdentifier(), - "Mechanic.DivingObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.DivingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.RepairPump".ToIdentifier(), - "Mechanic.RepairPumpObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.RepairPumpText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Mechanic.RepairSubmarine".ToIdentifier(), - "Mechanic.RepairSubmarineObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.RepairSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "tutorial.laddertitle".ToIdentifier(), - "tutorial.laddertitle".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "tutorial.ladderdescription".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) - { } - - protected override CharacterInfo GetCharacterInfo() - { - return new CharacterInfo( - CharacterPrefab.HumanSpeciesName, - jobOrJobPrefab: new Job( - JobPrefab.Prefabs["mechanic"], Rand.RandSync.Unsynced, 0, - new Skill("medical".ToIdentifier(), 0), - new Skill("weapons".ToIdentifier(), 0), - new Skill("mechanical".ToIdentifier(), 50), - new Skill("electrical".ToIdentifier(), 20), - new Skill("helm".ToIdentifier(), 0))); - } - - protected override void Initialize() - { - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); - mechanic = Character.Controlled; - - foreach (Item item in mechanic.Inventory.AllItemsMod) - { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } - item.Unequip(mechanic); - mechanic.Inventory.RemoveItem(item); - } - - var repairOrder = OrderPrefab.Prefabs["repairsystems"]; - mechanic_repairIcon = repairOrder.SymbolSprite; - mechanic_repairIconColor = repairOrder.Color; - mechanic_weldIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(1, 256, 127, 127), new Vector2(0.5f, 0.5f)); - - // Other tutorial items - tutorial_securityFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_securityfinaldoorlight")).GetComponent(); - tutorial_upperFinalDoor = Item.ItemList.Find(i => i.HasTag("tutorial_upperfinaldoor")).GetComponent(); - tutorial_submarineSteering = Item.ItemList.Find(i => i.HasTag("command")).GetComponent(); - - tutorial_submarineSteering.CanBeSelected = false; - foreach (ItemComponent ic in tutorial_submarineSteering.Item.Components) - { - ic.CanBeSelected = false; - } - - SetDoorAccess(null, tutorial_securityFinalDoorLight, false); - SetDoorAccess(tutorial_upperFinalDoor, null, false); - - // Room 1 - mechanic_firstDoor = Item.ItemList.Find(i => i.HasTag("mechanic_firstdoor")).GetComponent(); - mechanic_firstDoorLight = Item.ItemList.Find(i => i.HasTag("mechanic_firstdoorlight")).GetComponent(); - - SetDoorAccess(mechanic_firstDoor, mechanic_firstDoorLight, false); - - // Room 2 - mechanic_equipmentObjectiveSensor = Item.ItemList.Find(i => i.HasTag("mechanic_equipmentobjectivesensor")).GetComponent(); - mechanic_equipmentCabinet = Item.ItemList.Find(i => i.HasTag("mechanic_equipmentcabinet")).GetComponent(); - mechanic_secondDoor = Item.ItemList.Find(i => i.HasTag("mechanic_seconddoor")).GetComponent(); - mechanic_secondDoorLight = Item.ItemList.Find(i => i.HasTag("mechanic_seconddoorlight")).GetComponent(); - - SetDoorAccess(mechanic_secondDoor, mechanic_secondDoorLight, false); - - // Room 3 - mechanic_weldingObjectiveSensor = Item.ItemList.Find(i => i.HasTag("mechanic_weldingobjectivesensor")).GetComponent(); - mechanic_workingPump = Item.ItemList.Find(i => i.HasTag("mechanic_workingpump")).GetComponent(); - mechanic_thirdDoor = Item.ItemList.Find(i => i.HasTag("mechanic_thirddoor")).GetComponent(); - mechanic_thirdDoorLight = Item.ItemList.Find(i => i.HasTag("mechanic_thirddoorlight")).GetComponent(); - mechanic_brokenWall_1 = Structure.WallList.Find(i => i.SpecialTag == "mechanic_brokenwall_1"); - //mechanic_ladderSensor = Item.ItemList.Find(i => i.HasTag("mechanic_laddersensor")).GetComponent(); - - SetDoorAccess(mechanic_thirdDoor, mechanic_thirdDoorLight, false); - mechanic_brokenWall_1.Indestructible = false; - mechanic_brokenWall_1.SpriteColor = Color.White; - for (int i = 0; i < mechanic_brokenWall_1.SectionCount; i++) - { - mechanic_brokenWall_1.AddDamage(i, 85); - } - mechanic_brokenhull_1 = mechanic_brokenWall_1.Sections[0].gap.FlowTargetHull; - - // Room 4 - mechanic_craftingObjectiveSensor = Item.ItemList.Find(i => i.HasTag("mechanic_craftingobjectivesensor")).GetComponent(); - mechanic_deconstructor = Item.ItemList.Find(i => i.HasTag("mechanic_deconstructor")).GetComponent(); - mechanic_fabricator = Item.ItemList.Find(i => i.HasTag("mechanic_fabricator")).GetComponent(); - mechanic_craftingCabinet = Item.ItemList.Find(i => i.HasTag("mechanic_craftingcabinet")).GetComponent(); - mechanic_fourthDoor = Item.ItemList.Find(i => i.HasTag("mechanic_fourthdoor")).GetComponent(); - mechanic_fourthDoorLight = Item.ItemList.Find(i => i.HasTag("mechanic_fourthdoorlight")).GetComponent(); - - SetDoorAccess(mechanic_fourthDoor, mechanic_fourthDoorLight, false); - - // Room 5 - mechanic_fifthDoor = Item.ItemList.Find(i => i.HasTag("mechanic_fifthdoor")).GetComponent(); - mechanic_fifthDoorLight = Item.ItemList.Find(i => i.HasTag("mechanic_fifthdoorlight")).GetComponent(); - mechanic_fireSensor = Item.ItemList.Find(i => i.HasTag("mechanic_firesensor")).GetComponent(); - - SetDoorAccess(mechanic_fifthDoor, mechanic_fifthDoorLight, false); - - // Room 6 - mechanic_divingSuitObjectiveSensor = Item.ItemList.Find(i => i.HasTag("mechanic_divingsuitobjectivesensor")).GetComponent(); - mechanic_divingSuitContainer = Item.ItemList.Find(i => i.HasTag("mechanic_divingsuitcontainer")).GetComponent(); - foreach (Item item in mechanic_divingSuitContainer.Inventory.AllItems) - { - foreach (ItemComponent ic in item.Components) - { - ic.CanBePicked = true; - } - } - - mechanic_oxygenContainer = Item.ItemList.Find(i => i.HasTag("mechanic_oxygencontainer")).GetComponent(); - foreach (Item item in mechanic_oxygenContainer.Inventory.AllItems) - { - foreach (ItemComponent ic in item.Components) - { - ic.CanBePicked = true; - } - } - - tutorial_mechanicFinalDoor = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoor")).GetComponent(); - tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); - - SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, false); - - // Room 7 - mechanic_brokenPump = Item.ItemList.Find(i => i.HasTag("mechanic_brokenpump")).GetComponent(); - mechanic_brokenPump.Item.Indestructible = false; - mechanic_brokenPump.Item.Condition = 0; - mechanic_brokenPump.CanBeSelected = false; - mechanic_brokenPump.Item.GetComponent().CanBeSelected = false; - mechanic_brokenWall_2 = Structure.WallList.Find(i => i.SpecialTag == "mechanic_brokenwall_2"); - tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); - tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - - mechanic_brokenWall_2.Indestructible = false; - mechanic_brokenWall_2.SpriteColor = Color.White; - for (int i = 0; i < mechanic_brokenWall_2.SectionCount; i++) - { - mechanic_brokenWall_2.AddDamage(i, 85); - } - mechanic_brokenhull_2 = mechanic_brokenWall_2.Sections[0].gap.FlowTargetHull; - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); - - // Submarine - tutorial_enteredSubmarineSensor = Item.ItemList.Find(i => i.HasTag("tutorial_enteredsubmarinesensor")).GetComponent(); - mechanic_submarineEngine = Item.ItemList.Find(i => i.HasTag("mechanic_submarineengine")).GetComponent(); - mechanic_submarineEngine.Item.Indestructible = false; - mechanic_submarineEngine.Item.Condition = 0f; - mechanic_ballastPump_1 = Item.ItemList.Find(i => i.HasTag("mechanic_ballastpump_1")).GetComponent(); - mechanic_ballastPump_1.Item.Indestructible = false; - mechanic_ballastPump_1.Item.Condition = 0f; - mechanic_ballastPump_2 = Item.ItemList.Find(i => i.HasTag("mechanic_ballastpump_2")).GetComponent(); - mechanic_ballastPump_2.Item.Indestructible = false; - mechanic_ballastPump_2.Item.Condition = 0f; - - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Started"); - GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); - } - - public override void Update(float deltaTime) - { - if (mechanic_brokenhull_1 != null) - { - mechanic_brokenhull_1.WaterVolume = MathHelper.Clamp(mechanic_brokenhull_1.WaterVolume, 0, mechanic_brokenhull_1.Volume * 0.85f); - } - base.Update(deltaTime); - } - - public override IEnumerable UpdateState() - { - while (GameMain.Instance.LoadingScreenOpen) yield return null; - - // Room 1 - SoundPlayer.PlayDamageSound("StructureBlunt", 10, Character.Controlled.WorldPosition); - while (shakeTimer > 0.0f) // Wake up, shake - { - shakeTimer -= 0.1f; - GameMain.GameScreen.Cam.Shake = shakeAmount; - yield return new WaitForSeconds(0.1f, false); - } - yield return new WaitForSeconds(2.5f, false); - - mechanic_fabricator.RemoveFabricationRecipes(allowedIdentifiers: - new[] { "extinguisher", "wrench", "weldingtool", "weldingfuel", "divingmask", "railgunshell", "nuclearshell", "uex", "harpoongun" }.ToIdentifiers()); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.WakeUp"), ChatMessageType.Radio, null); - - yield return new WaitForSeconds(2.5f, false); - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Up), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Left), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Down), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Right), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Open door objective - yield return new WaitForSeconds(0.0f, false); - SetDoorAccess(mechanic_firstDoor, mechanic_firstDoorLight, true); - SetHighlight(mechanic_firstDoor.Item, true); - do { yield return null; } while (!mechanic_firstDoor.IsOpen); - SetHighlight(mechanic_firstDoor.Item, false); - yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(0); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective0"); - - // Room 2 - yield return new WaitForSeconds(0.0f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Equipment"), ChatMessageType.Radio, null); - do { yield return null; } while (!mechanic_equipmentObjectiveSensor.MotionDetected); - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Equipment & inventory objective - SetHighlight(mechanic_equipmentCabinet.Item, true); - bool firstSlotRemoved = false; - bool secondSlotRemoved = false; - bool thirdSlotRemoved = false; - do - { - if (IsSelectedItem(mechanic_equipmentCabinet.Item)) - { - if (!firstSlotRemoved) - { - HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } - } - - if (!secondSlotRemoved) - { - HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } - } - - if (!thirdSlotRemoved) - { - HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } - } - - for (int i = 0; i < mechanic.Inventory.Capacity; i++) - { - if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - - yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("divingmask".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("weldingtool".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("wrench".ToIdentifier()) == null); // Wait until looted - SetHighlight(mechanic_equipmentCabinet.Item, false); - yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(1); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective1"); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Breach"), ChatMessageType.Radio, null); - - // Room 3 - do { yield return null; } while (!mechanic_weldingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Welding objective - do - { - if (!mechanic.HasEquippedItem("divingmask".ToIdentifier())) - { - HighlightInventorySlot(mechanic.Inventory, "divingmask".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - - if (!mechanic.HasEquippedItem("weldingtool".ToIdentifier())) - { - HighlightInventorySlot(mechanic.Inventory, "weldingtool".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - yield return null; - } while (!mechanic.HasEquippedItem("divingmask".ToIdentifier()) || !mechanic.HasEquippedItem("weldingtool".ToIdentifier())); // Wait until equipped - SetDoorAccess(mechanic_secondDoor, mechanic_secondDoorLight, true); - mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_1, mechanic_weldIcon, mechanic_repairIconColor); - do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_1)); // Highlight until repaired - mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_1); - RemoveCompletedObjective(2); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective2"); - - yield return new WaitForSeconds(1f, false); - TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Pump objective - SetHighlight(mechanic_workingPump.Item, true); - do - { - yield return null; - if (IsSelectedItem(mechanic_workingPump.Item)) - { - if (mechanic_workingPump.PowerButton.FlashTimer <= 0) - { - mechanic_workingPump.PowerButton.Flash(uiHighlightColor, 1.5f, true); - } - } - } while (mechanic_workingPump.FlowPercentage >= 0 || !mechanic_workingPump.IsActive); // Highlight until draining - SetHighlight(mechanic_workingPump.Item, false); - do { yield return null; } while (mechanic_brokenhull_1 != null && mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained - RemoveCompletedObjective(3); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective3"); - - SetDoorAccess(mechanic_thirdDoor, mechanic_thirdDoorLight, true); - //TriggerTutorialSegment(11, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select]); // Ladder objective - //do { yield return null; } while (!mechanic_ladderSensor.MotionDetected); - //RemoveCompletedObjective(segments[11]); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.News"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(1f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Fire"), ChatMessageType.Radio, null); - - // Room 4 - do { yield return null; } while (!mechanic_thirdDoor.IsOpen); - yield return new WaitForSeconds(1f, false); - mechanic_fire = new DummyFireSource(new Vector2(20f, 2f), Item.ItemList.Find(i => i.HasTag("mechanic_fire")).WorldPosition); - //do { yield return null; } while (!mechanic_craftingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(4); // Deconstruct - - SetHighlight(mechanic_craftingCabinet.Item, true); - - bool gotOxygenTank = false; - bool gotSodium = false; - do - { - if (mechanic.SelectedConstruction == mechanic_craftingCabinet.Item) - { - for (int i = 0; i < mechanic.Inventory.Capacity; i++) - { - if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - - if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null && mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null) - { - for (int i = 0; i < mechanic_craftingCabinet.Capacity; i++) - { - Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.Prefab.Identifier == "oxygentank") - { - HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } - } - - if (mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) == null) - { - for (int i = 0; i < mechanic_craftingCabinet.Inventory.Capacity; i++) - { - Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.Prefab.Identifier == "sodium") - { - HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } - } - } - - if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null || - mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null)) - { - gotOxygenTank = true; - } - if (!gotSodium && mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) - { - gotSodium = true; - } - yield return null; - } while (!gotOxygenTank || !gotSodium); // Wait until looted - - yield return new WaitForSeconds(1.0f, false); - SetHighlight(mechanic_craftingCabinet.Item, false); - SetHighlight(mechanic_deconstructor.Item, true); - do - { - if (IsSelectedItem(mechanic_deconstructor.Item)) - { - if (mechanic_deconstructor.OutputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null) - { - HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); - - for (int i = 0; i < mechanic.Inventory.Capacity; i++) - { - if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - else - { - if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null) - { - HighlightInventorySlot(mechanic.Inventory, "oxygentank".ToIdentifier(), highlightColor, .5f, .5f, 0f); - for (int i = 0; i < mechanic_deconstructor.InputContainer.Inventory.Capacity; i++) - { - HighlightInventorySlot(mechanic_deconstructor.InputContainer.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } - - if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && !mechanic_deconstructor.IsActive) - { - if (mechanic_deconstructor.ActivateButton.FlashTimer <= 0) - { - mechanic_deconstructor.ActivateButton.Flash(highlightColor, 1.5f, false); - } - } - } - } - yield return null; - } while ( - mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null && - mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null); // Wait until aluminium obtained - - SetHighlight(mechanic_deconstructor.Item, false); - RemoveCompletedObjective(4); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective4"); - - yield return new WaitForSeconds(1f, false); - TriggerTutorialSegment(5); // Fabricate - SetHighlight(mechanic_fabricator.Item, true); - do - { - if (IsSelectedItem(mechanic_fabricator.Item)) - { - if (mechanic_fabricator.SelectedItem?.TargetItem.Identifier != "extinguisher") - { - mechanic_fabricator.HighlightRecipe("extinguisher", highlightColor); - } - else - { - if (mechanic_fabricator.OutputContainer.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) != null) - { - HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher".ToIdentifier(), highlightColor, .5f, .5f, 0f); - - /*for (int i = 0; i < mechanic.Inventory.Capacity; i++) - { - if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); - }*/ - } - else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null && !mechanic_fabricator.IsActive) - { - if (mechanic_fabricator.ActivateButton.FlashTimer <= 0) - { - mechanic_fabricator.ActivateButton.Flash(highlightColor, 1.5f, false); - } - } - else if (mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null || mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) - { - HighlightInventorySlot(mechanic.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); - HighlightInventorySlot(mechanic.Inventory, "sodium".ToIdentifier(), highlightColor, .5f, .5f, 0f); - - if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(0) == null) - { - HighlightInventorySlot(mechanic_fabricator.InputContainer.Inventory, 0, highlightColor, .5f, .5f, 0f); - } - - if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(1) == null) - { - HighlightInventorySlot(mechanic_fabricator.InputContainer.Inventory, 1, highlightColor, .5f, .5f, 0f); - } - } - } - } - yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) == null); // Wait until extinguisher is created - RemoveCompletedObjective(5); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective5"); - SetHighlight(mechanic_fabricator.Item, false); - SetDoorAccess(mechanic_fourthDoor, mechanic_fourthDoorLight, true); - - // Room 5 - do { yield return null; } while (!mechanic_fireSensor.MotionDetected); - TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Using the extinguisher - do { yield return null; } while (!mechanic_fire.Removed); // Wait until extinguished - yield return new WaitForSeconds(3f, false); - RemoveCompletedObjective(6); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective6"); - - if (mechanic.HasEquippedItem("extinguisher".ToIdentifier())) // do not trigger if dropped already - { - TriggerTutorialSegment(7); - do - { - HighlightInventorySlot(mechanic.Inventory, "extinguisher".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - yield return null; - } while (mechanic.HasEquippedItem("extinguisher".ToIdentifier())); - RemoveCompletedObjective(7); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective7"); - } - SetDoorAccess(mechanic_fifthDoor, mechanic_fifthDoorLight, true); - - // Room 6 - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Diving"), ChatMessageType.Radio, null); - do { yield return null; } while (!mechanic_divingSuitObjectiveSensor.MotionDetected); - TriggerTutorialSegment(8); // Dangers of pressure, equip diving suit objective - SetHighlight(mechanic_divingSuitContainer.Item, true); - do - { - if (IsSelectedItem(mechanic_divingSuitContainer.Item)) - { - if (mechanic_divingSuitContainer.Inventory.visualSlots != null) - { - for (int i = 0; i < mechanic_divingSuitContainer.Inventory.Capacity; i++) - { - HighlightInventorySlot(mechanic_divingSuitContainer.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); - } - } - } - yield return null; - } while (!mechanic.HasEquippedItem("divingsuit".ToIdentifier(), slotType: InvSlotType.OuterClothes)); - SetHighlight(mechanic_divingSuitContainer.Item, false); - RemoveCompletedObjective(8); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective8"); - SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); - - // Room 7 - mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_2, mechanic_weldIcon, mechanic_repairIconColor); - do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_2)); - mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_2); - yield return new WaitForSeconds(2f, false); - - TriggerTutorialSegment(9, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)); // Repairing machinery (pump) - SetHighlight(mechanic_brokenPump.Item, true); - mechanic_brokenPump.CanBeSelected = true; - Repairable repairablePumpComponent = mechanic_brokenPump.Item.GetComponent(); - repairablePumpComponent.CanBeSelected = true; - do - { - yield return null; - if (repairablePumpComponent.IsBelowRepairThreshold) - { - if (!mechanic.HasEquippedItem("wrench".ToIdentifier())) - { - HighlightInventorySlot(mechanic.Inventory, "wrench".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - } - else if (IsSelectedItem(mechanic_brokenPump.Item) && repairablePumpComponent.CurrentFixer == null) - { - if (repairablePumpComponent.RepairButton.FlashTimer <= 0) - { - repairablePumpComponent.RepairButton.Flash(); - } - } - } - else - { - if (IsSelectedItem(mechanic_brokenPump.Item)) - { - if (mechanic_brokenPump.PowerButton.FlashTimer <= 0) - { - mechanic_brokenPump.PowerButton.Flash(uiHighlightColor, 1.5f, true); - } - } - } - } while (repairablePumpComponent.IsBelowRepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); - RemoveCompletedObjective(9); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective9"); - SetHighlight(mechanic_brokenPump.Item, false); - do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - - // Submarine - do { yield return null; } while (!tutorial_enteredSubmarineSensor.MotionDetected); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Submarine"), ChatMessageType.Radio, null); - TriggerTutorialSegment(10); // Repairing ballast pumps, engine - while (ContentRunning) yield return null; - mechanic.AddActiveObjectiveEntity(mechanic_ballastPump_1.Item, mechanic_repairIcon, mechanic_repairIconColor); - mechanic.AddActiveObjectiveEntity(mechanic_ballastPump_2.Item, mechanic_repairIcon, mechanic_repairIconColor); - mechanic.AddActiveObjectiveEntity(mechanic_submarineEngine.Item, mechanic_repairIcon, mechanic_repairIconColor); - SetHighlight(mechanic_ballastPump_1.Item, true); - SetHighlight(mechanic_ballastPump_2.Item, true); - SetHighlight(mechanic_submarineEngine.Item, true); - - Repairable repairablePumpComponent1 = mechanic_ballastPump_1.Item.GetComponent(); - Repairable repairablePumpComponent2 = mechanic_ballastPump_2.Item.GetComponent(); - Repairable repairableEngineComponent = mechanic_submarineEngine.Item.GetComponent(); - - // Remove highlights when each individual machine is repaired - do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (repairablePumpComponent1.IsBelowRepairThreshold || repairablePumpComponent2.IsBelowRepairThreshold || repairableEngineComponent.IsBelowRepairThreshold); - CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); - RemoveCompletedObjective(10); - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective10"); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); - - // END TUTORIAL - GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Completed"); - CoroutineManager.StartCoroutine(TutorialCompleted()); - } - - private bool IsSelectedItem(Item item) - { - return mechanic?.SelectedConstruction == item; - } - - private bool WallHasDamagedSections(Structure wall) - { - for (int i = 0; i < wall.SectionCount; i++) - { - if (wall.Sections[i].damage > 0) return true; - } - - return false; - } - - private void CheckHighlights(Repairable comp1, Repairable comp2, Repairable comp3) - { - if (!comp1.IsBelowRepairThreshold && mechanic_ballastPump_1.Item.ExternalHighlight) - { - SetHighlight(mechanic_ballastPump_1.Item, false); - mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_1.Item); - } - if (!comp2.IsBelowRepairThreshold && mechanic_ballastPump_2.Item.ExternalHighlight) - { - SetHighlight(mechanic_ballastPump_2.Item, false); - mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_2.Item); - } - if (!comp3.IsBelowRepairThreshold && mechanic_submarineEngine.Item.ExternalHighlight) - { - SetHighlight(mechanic_submarineEngine.Item, false); - mechanic.RemoveActiveObjectiveEntity(mechanic_submarineEngine.Item); - } - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs deleted file mode 100644 index dd0f49fe5..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ /dev/null @@ -1,526 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Xml.Linq; -using System.Linq; -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; - -namespace Barotrauma.Tutorials -{ - class OfficerTutorial : ScenarioTutorial - { - // Other tutorial items - private LightComponent tutorial_mechanicFinalDoorLight; - private Steering tutorial_submarineSteering; - - // Room 1 - private float shakeTimer = 1f; - private float shakeAmount = 20f; - - // Room 2 - private MotionSensor officer_equipmentObjectiveSensor; - private ItemContainer officer_equipmentCabinet; - private Door officer_firstDoor; - private LightComponent officer_firstDoorLight; - - // Room 3 - private MotionSensor officer_crawlerSensor; - private Character officer_crawler; - private Vector2 officer_crawlerSpawnPos; - private Door officer_secondDoor; - private LightComponent officer_secondDoorLight; - - // Room 4 - private MotionSensor officer_somethingBigSensor; - private ItemContainer officer_coilgunLoader; - private ItemContainer officer_ammoShelf_1; - private ItemContainer officer_ammoShelf_2; - private PowerContainer officer_superCapacitor; - private Item officer_coilgunPeriscope; - private Character officer_hammerhead; - private Vector2 officer_hammerheadSpawnPos; - private Door officer_thirdDoor; - private LightComponent officer_thirdDoorLight; - - // Room 5 - private MotionSensor officer_rangedWeaponSensor; - private ItemContainer officer_rangedWeaponCabinet; - private ItemContainer officer_rangedWeaponHolder; - private Door officer_fourthDoor; - private LightComponent officer_fourthDoorLight; - - // Room 6 - private MotionSensor officer_mudraptorObjectiveSensor; - private Vector2 officer_mudraptorSpawnPos; - private Character officer_mudraptor; - private Door tutorial_securityFinalDoor; - private LightComponent tutorial_securityFinalDoorLight; - - // Submarine - private Door tutorial_submarineDoor; - private LightComponent tutorial_submarineDoorLight; - private MotionSensor tutorial_enteredSubmarineSensor; - private Item officer_subAmmoBox_1; - private Item officer_subAmmoBox_2; - private ItemContainer officer_subAmmoShelf; - private ItemContainer officer_subLoader_1; - private ItemContainer officer_subLoader_2; - private PowerContainer officer_subSuperCapacitor_1; - private PowerContainer officer_subSuperCapacitor_2; - - // Variables - private LocalizedString radioSpeakerName; - private Character officer; - private float superCapacitorRechargeRate = 10; - private Sprite officer_gunIcon; - private Color officer_gunIconColor; - - public OfficerTutorial() : base("tutorial.securityofficertraining".ToIdentifier(), - new Segment( - "Mechanic.Equipment".ToIdentifier(), - "Mechanic.EquipmentObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Officer.MeleeWeapon".ToIdentifier(), - "Officer.MeleeWeaponObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Officer.MeleeWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Officer.Crawler".ToIdentifier(), - "Officer.CrawlerObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Officer.CrawlerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Officer.SomethingBig".ToIdentifier(), - "Officer.SomethingBigObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_loaders.webm", TextTag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80 }), - new Segment( - "Officer.Hammerhead".ToIdentifier(), - "Officer.HammerheadObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Officer.HammerheadText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Officer.RangedWeapon".ToIdentifier(), - "Officer.RangedWeaponObjective".ToIdentifier(), - TutorialContentType.ManualVideo, - textContent: new Segment.Text { Tag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, - videoContent: new Segment.Video { File = "tutorial_ranged.webm", TextTag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80 }), - new Segment( - "Officer.Mudraptor".ToIdentifier(), - "Officer.MudraptorObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Officer.MudraptorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), - new Segment( - "Officer.ArmSubmarine".ToIdentifier(), - "Officer.ArmSubmarineObjective".ToIdentifier(), - TutorialContentType.TextOnly, - textContent: new Segment.Text { Tag = "Officer.ArmSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) - { } - - protected override CharacterInfo GetCharacterInfo() - { - return new CharacterInfo( - CharacterPrefab.HumanSpeciesName, - jobOrJobPrefab: new Job( - JobPrefab.Prefabs["securityofficer"], Rand.RandSync.Unsynced, 0, - new Skill("medical".ToIdentifier(), 20), - new Skill("weapons".ToIdentifier(), 70), - new Skill("mechanical".ToIdentifier(), 20), - new Skill("electrical".ToIdentifier(), 20), - new Skill("helm".ToIdentifier(), 20))); - } - - protected override void Initialize() - { - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); - officer = Character.Controlled; - - foreach (Item item in officer.Inventory.AllItemsMod) - { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } - item.Unequip(officer); - officer.Inventory.RemoveItem(item); - } - - var gunOrder = OrderPrefab.Prefabs["operateweapons"]; - officer_gunIcon = gunOrder.SymbolSprite; - officer_gunIconColor = gunOrder.Color; - - var bandage = FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); - bandage.Unequip(officer); - officer.Inventory.RemoveItem(bandage); - FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); - - // Other tutorial items - tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); - tutorial_submarineSteering = Item.ItemList.Find(i => i.HasTag("command")).GetComponent(); - - tutorial_submarineSteering.CanBeSelected = false; - foreach (ItemComponent ic in tutorial_submarineSteering.Item.Components) - { - ic.CanBeSelected = false; - } - - SetDoorAccess(null, tutorial_mechanicFinalDoorLight, false); - - // Room 2 - officer_equipmentObjectiveSensor = Item.ItemList.Find(i => i.HasTag("officer_equipmentobjectivesensor")).GetComponent(); - officer_equipmentCabinet = Item.ItemList.Find(i => i.HasTag("officer_equipmentcabinet")).GetComponent(); - officer_firstDoor = Item.ItemList.Find(i => i.HasTag("officer_firstdoor")).GetComponent(); - officer_firstDoorLight = Item.ItemList.Find(i => i.HasTag("officer_firstdoorlight")).GetComponent(); - - SetDoorAccess(officer_firstDoor, officer_firstDoorLight, false); - - // Room 3 - officer_crawlerSensor = Item.ItemList.Find(i => i.HasTag("officer_crawlerobjectivesensor")).GetComponent(); - officer_crawlerSpawnPos = Item.ItemList.Find(i => i.HasTag("officer_crawlerspawn")).WorldPosition; - officer_secondDoor = Item.ItemList.Find(i => i.HasTag("officer_seconddoor")).GetComponent(); - officer_secondDoorLight = Item.ItemList.Find(i => i.HasTag("officer_seconddoorlight")).GetComponent(); - - SetDoorAccess(officer_secondDoor, officer_secondDoorLight, false); - - // Room 4 - officer_somethingBigSensor = Item.ItemList.Find(i => i.HasTag("officer_somethingbigobjectivesensor")).GetComponent(); - officer_coilgunLoader = Item.ItemList.Find(i => i.HasTag("officer_coilgunloader")).GetComponent(); - officer_superCapacitor = Item.ItemList.Find(i => i.HasTag("officer_supercapacitor")).GetComponent(); - officer_coilgunPeriscope = Item.ItemList.Find(i => i.HasTag("officer_coilgunperiscope")); - officer_hammerheadSpawnPos = Item.ItemList.Find(i => i.HasTag("officer_hammerheadspawn")).WorldPosition; - officer_thirdDoor = Item.ItemList.Find(i => i.HasTag("officer_thirddoor")).GetComponent(); - officer_thirdDoorLight = Item.ItemList.Find(i => i.HasTag("officer_thirddoorlight")).GetComponent(); - officer_ammoShelf_1 = Item.ItemList.Find(i => i.HasTag("officer_ammoshelf_1")).GetComponent(); - officer_ammoShelf_2 = Item.ItemList.Find(i => i.HasTag("officer_ammoshelf_2")).GetComponent(); - - SetDoorAccess(officer_thirdDoor, officer_thirdDoorLight, false); - - // Room 5 - officer_rangedWeaponSensor = Item.ItemList.Find(i => i.HasTag("officer_rangedweaponobjectivesensor")).GetComponent(); - officer_rangedWeaponCabinet = Item.ItemList.Find(i => i.HasTag("officer_rangedweaponcabinet")).GetComponent(); - officer_rangedWeaponHolder = Item.ItemList.Find(i => i.HasTag("officer_rangedweaponholder")).GetComponent(); - officer_fourthDoor = Item.ItemList.Find(i => i.HasTag("officer_fourthdoor")).GetComponent(); - officer_fourthDoorLight = Item.ItemList.Find(i => i.HasTag("officer_fourthdoorlight")).GetComponent(); - - SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, false); - - // Room 6 - officer_mudraptorObjectiveSensor = Item.ItemList.Find(i => i.HasTag("officer_mudraptorobjectivesensor")).GetComponent(); - officer_mudraptorSpawnPos = Item.ItemList.Find(i => i.HasTag("officer_mudraptorspawn")).WorldPosition; - tutorial_securityFinalDoor = Item.ItemList.Find(i => i.HasTag("tutorial_securityfinaldoor")).GetComponent(); - tutorial_securityFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_securityfinaldoorlight")).GetComponent(); - - SetDoorAccess(tutorial_securityFinalDoor, tutorial_securityFinalDoorLight, false); - - // Submarine - tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); - tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - tutorial_enteredSubmarineSensor = Item.ItemList.Find(i => i.HasTag("tutorial_enteredsubmarinesensor")).GetComponent(); - officer_subAmmoBox_1 = Item.ItemList.Find(i => i.HasTag("officer_subammobox_1")); - officer_subAmmoBox_2 = Item.ItemList.Find(i => i.HasTag("officer_subammobox_2")); - officer_subLoader_1 = Item.ItemList.Find(i => i.HasTag("officer_subloader_1")).GetComponent(); - officer_subLoader_2 = Item.ItemList.Find(i => i.HasTag("officer_subloader_2")).GetComponent(); - officer_subSuperCapacitor_1 = Item.ItemList.Find(i => i.HasTag("officer_subsupercapacitor_1")).GetComponent(); - officer_subSuperCapacitor_2 = Item.ItemList.Find(i => i.HasTag("officer_subsupercapacitor_2")).GetComponent(); - officer_subAmmoShelf = Item.ItemList.Find(i => i.HasTag("officer_subammoshelf")).GetComponent(); - SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Started"); - GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); - } - - public override IEnumerable UpdateState() - { - while (GameMain.Instance.LoadingScreenOpen) yield return null; - - yield return new WaitForSeconds(0.01f); - - // Room 1 - SoundPlayer.PlayDamageSound("StructureBlunt", 10, Character.Controlled.WorldPosition); - while (shakeTimer > 0.0f) // Wake up, shake - { - shakeTimer -= 0.1f; - GameMain.GameScreen.Cam.Shake = shakeAmount; - yield return new WaitForSeconds(0.1f, false); - } - - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.WakeUp"), ChatMessageType.Radio, null); - - // Room 2 - do { yield return null; } while (!officer_equipmentObjectiveSensor.MotionDetected); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Equipment"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(3f, false); - //TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect]); // Retrieve equipment - SetHighlight(officer_equipmentCabinet.Item, true); - bool firstSlotRemoved = false; - bool secondSlotRemoved = false; - bool thirdSlotRemoved = false; - do - { - if (IsSelectedItem(officer_equipmentCabinet.Item)) - { - if (!firstSlotRemoved) - { - HighlightInventorySlot(officer_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } - } - - if (!secondSlotRemoved) - { - HighlightInventorySlot(officer_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } - } - - if (!thirdSlotRemoved) - { - HighlightInventorySlot(officer_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } - } - - for (int i = 0; i < officer.Inventory.visualSlots.Length; i++) - { - if (officer.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(officer.Inventory, i, highlightColor, .5f, .5f, 0f); } - } - } - - yield return null; - } while (!officer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted - //RemoveCompletedObjective(segments[0]); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective0"); - SetHighlight(officer_equipmentCabinet.Item, false); - do { yield return null; } while (IsSelectedItem(officer_equipmentCabinet.Item)); - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor - do - { - if (!officer.HasEquippedItem("stunbaton".ToIdentifier())) - { - HighlightInventorySlot(officer.Inventory, "stunbaton".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - if (!officer.HasEquippedItem("bodyarmor".ToIdentifier())) - { - HighlightInventorySlot(officer.Inventory, "bodyarmor".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - if (!officer.HasEquippedItem("ballistichelmet1".ToIdentifier())) - { - HighlightInventorySlot(officer.Inventory, "ballistichelmet1".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - yield return new WaitForSeconds(1f, false); - } while (!officer.HasEquippedItem("stunbaton".ToIdentifier()) || !officer.HasEquippedItem("bodyarmor".ToIdentifier()) || !officer.HasEquippedItem("ballistichelmet1".ToIdentifier())); - RemoveCompletedObjective(1); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective1"); - SetDoorAccess(officer_firstDoor, officer_firstDoorLight, true); - - // Room 3 - do { yield return null; } while (!officer_crawlerSensor.MotionDetected); - TriggerTutorialSegment(2); - officer_crawler = SpawnMonster("crawler", officer_crawlerSpawnPos); - do { yield return null; } while (!officer_crawler.IsDead); - RemoveCompletedObjective(2); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective2"); - Heal(officer); - yield return new WaitForSeconds(1f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.CrawlerDead"), ChatMessageType.Radio, null); - SetDoorAccess(officer_secondDoor, officer_secondDoorLight, true); - - // Room 4 - do { yield return null; } while (!officer_somethingBigSensor.MotionDetected); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.SomethingBig"), ChatMessageType.Radio, null); - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(3); // Arm coilgun - do - { - SetHighlight(officer_coilgunLoader.Item, officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); - HighlightInventorySlot(officer_coilgunLoader.Inventory, 0, highlightColor, .5f, .5f, 0f); - SetHighlight(officer_superCapacitor.Item, officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate); - SetHighlight(officer_ammoShelf_1.Item, officer_coilgunLoader.Item.ExternalHighlight ); - SetHighlight(officer_ammoShelf_2.Item, officer_coilgunLoader.Item.ExternalHighlight ); - if (IsSelectedItem(officer_coilgunLoader.Item)) - { - HighlightInventorySlot(officer.Inventory, "coilgunammobox".ToIdentifier(), highlightColor, .5f, .5f, 0f); - } - yield return null; - } while (officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); - SetHighlight(officer_coilgunLoader.Item, false); - SetHighlight(officer_superCapacitor.Item, false); - SetHighlight(officer_ammoShelf_1.Item, false); - SetHighlight(officer_ammoShelf_2.Item, false); - RemoveCompletedObjective(3); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective3"); - - yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Kill hammerhead - officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); - officer_hammerhead.Params.AI.AvoidAbyss = false; - officer_hammerhead.Params.AI.StayInAbyss = false; - officer_hammerhead.AIController.SelectTarget(officer.AiTarget); - SetHighlight(officer_coilgunPeriscope, true); - float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); - do - { - float distance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerhead.WorldPosition); - if (distance > originalDistance * 1.5f || officer_hammerhead.WorldPosition.Y > officer_coilgunPeriscope.WorldPosition.Y) - { - // Don't let the Hammerhead go too far. - officer_hammerhead.TeleportTo(officer_hammerheadSpawnPos + new Vector2(0, -1000)); - } - if (distance > originalDistance) - { - // Ensure that the Hammerhead targets the player - officer.AiTarget.SoundRange = float.MaxValue; - officer.AiTarget.SightRange = float.MaxValue; - officer_hammerhead.AIController.SelectTarget(officer.AiTarget); - if ((officer_hammerhead.AIController as EnemyAIController)?.SelectedTargetingParams != null) - { - ((EnemyAIController)officer_hammerhead.AIController).SelectedTargetingParams.ReactDistance = 5000.0f; - } - /*var ai = officer_hammerhead.AIController as EnemyAIController; - ai.sight = 2.0f;*/ - } - yield return null; - } - while(!officer_hammerhead.IsDead); - Heal(officer); - SetHighlight(officer_coilgunPeriscope, false); - RemoveCompletedObjective(4); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective4"); - - yield return new WaitForSeconds(1f, false); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.HammerheadDead"), ChatMessageType.Radio, null); - SetDoorAccess(officer_thirdDoor, officer_thirdDoorLight, true); - - // Room 5 - //do { yield return null; } while (!officer_rangedWeaponSensor.MotionDetected); - do { yield return null; } while (!officer_thirdDoor.IsOpen); - yield return new WaitForSeconds(3f, false); - TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Ranged weapons - SetHighlight(officer_rangedWeaponHolder.Item, true); - do { yield return null; } while (!officer_rangedWeaponHolder.Inventory.IsEmpty()); // Wait until looted - SetHighlight(officer_rangedWeaponHolder.Item, false); - do - { - HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - yield return null; - } while (!officer.HasEquippedItem("shotgun".ToIdentifier())); // Wait until equipped - ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun".ToIdentifier()).GetComponent(); - SetHighlight(officer_rangedWeaponCabinet.Item, true); - do - { - if (IsSelectedItem(officer_rangedWeaponCabinet.Item)) - { - if (officer_rangedWeaponCabinet.Inventory.visualSlots != null) - { - for (int i = 0; i < officer_rangedWeaponCabinet.Inventory.Capacity; i++) - { - if (officer_rangedWeaponCabinet.Inventory.GetItemAt(i)?.Prefab.Identifier == "shotgunshell") - { - HighlightInventorySlot(officer_rangedWeaponCabinet.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); - } - } - } - } - - for (int i = 0; i < officer.Inventory.Capacity; i++) - { - if (officer.Inventory.GetItemAt(i)?.Prefab.Identifier == "shotgunshell") - { - HighlightInventorySlot(officer.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); - } - } - - if (officer.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null)) - { - HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); - } - yield return null; - } while (!shotGunChamber.Inventory.IsFull(takeStacksIntoAccount: true)); // Wait until all six harpoons loaded - RemoveCompletedObjective(5); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective5"); - SetHighlight(officer_rangedWeaponCabinet.Item, false); - SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); - - // Room 6 - do { yield return null; } while (!officer_mudraptorObjectiveSensor.MotionDetected); - TriggerTutorialSegment(6); - officer_mudraptor = SpawnMonster("mudraptor", officer_mudraptorSpawnPos); - do { yield return null; } while (!officer_mudraptor.IsDead); - Heal(officer); - RemoveCompletedObjective(6); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective6"); - SetDoorAccess(tutorial_securityFinalDoor, tutorial_securityFinalDoorLight, true); - - // Submarine - do { yield return null; } while (!tutorial_enteredSubmarineSensor.MotionDetected); - TriggerTutorialSegment(7); - while (ContentRunning) yield return null; - officer.AddActiveObjectiveEntity(officer_subAmmoBox_1, officer_gunIcon, officer_gunIconColor); - officer.AddActiveObjectiveEntity(officer_subAmmoBox_2, officer_gunIcon, officer_gunIconColor); - officer.AddActiveObjectiveEntity(officer_subSuperCapacitor_1.Item, officer_gunIcon, officer_gunIconColor); - officer.AddActiveObjectiveEntity(officer_subSuperCapacitor_2.Item, officer_gunIcon, officer_gunIconColor); - SetHighlight(officer_subSuperCapacitor_1.Item, true); - SetHighlight(officer_subSuperCapacitor_2.Item, true); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Submarine"), ChatMessageType.Radio, null); - do - { - SetHighlight(officer_subLoader_1.Item, officer_subLoader_1.Inventory.GetItemAt(0) == null || officer_subLoader_1.Inventory.GetItemAt(0).Condition == 0); - SetHighlight(officer_subLoader_2.Item, officer_subLoader_2.Inventory.GetItemAt(0) == null || officer_subLoader_2.Inventory.GetItemAt(0).Condition == 0); - HighlightInventorySlot(officer_subLoader_1.Inventory, 0, highlightColor, .5f, .5f, 0f); - HighlightInventorySlot(officer_subLoader_2.Inventory, 0, highlightColor, .5f, .5f, 0f); - - if (officer_subSuperCapacitor_1.Item.ExternalHighlight && officer_subSuperCapacitor_1.RechargeSpeed >= superCapacitorRechargeRate) - { - SetHighlight(officer_subSuperCapacitor_1.Item, false); - officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_1.Item); - } - - if (officer_subSuperCapacitor_2.Item.ExternalHighlight && officer_subSuperCapacitor_2.RechargeSpeed >= superCapacitorRechargeRate) - { - SetHighlight(officer_subSuperCapacitor_2.Item, false); - officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_2.Item); - } - - SetHighlight(officer_subAmmoBox_1, officer_subAmmoBox_1.ParentInventory != officer_subLoader_1.Inventory && officer_subAmmoBox_1.ParentInventory != officer_subLoader_2.Inventory); - SetHighlight(officer_subAmmoBox_2, officer_subAmmoBox_2.ParentInventory != officer_subLoader_1.Inventory && officer_subAmmoBox_2.ParentInventory != officer_subLoader_2.Inventory); - SetHighlight(officer_subAmmoShelf.Item, officer_subLoader_1.Item.ExternalHighlight || officer_subLoader_2.Item.ExternalHighlight); - if (officer_subAmmoBox_1.ParentInventory == officer_subLoader_1.Inventory || officer_subAmmoBox_1.ParentInventory == officer_subLoader_2.Inventory) officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_1); - if (officer_subAmmoBox_2.ParentInventory == officer_subLoader_1.Inventory || officer_subAmmoBox_2.ParentInventory == officer_subLoader_2.Inventory) officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_2); - yield return null; - } while (officer_subLoader_1.Item.ExternalHighlight || officer_subLoader_2.Item.ExternalHighlight || officer_subSuperCapacitor_1.Item.ExternalHighlight || officer_subSuperCapacitor_2.Item.ExternalHighlight); - SetHighlight(officer_subLoader_1.Item, false); - SetHighlight(officer_subLoader_2.Item, false); - SetHighlight(officer_subSuperCapacitor_1.Item, false); - SetHighlight(officer_subSuperCapacitor_2.Item, false); - SetHighlight(officer_subAmmoBox_1, false); - SetHighlight(officer_subAmmoBox_2, false); - SetHighlight(officer_subAmmoShelf.Item, false); - officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_1.Item); - officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_2.Item); - officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_1); - officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_2); - RemoveCompletedObjective(7); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective7"); - GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Complete"), ChatMessageType.Radio, null); - - yield return new WaitForSeconds(4f, false); - GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Completed"); - CoroutineManager.StartCoroutine(TutorialCompleted()); - } - - private bool IsSelectedItem(Item item) - { - return officer?.SelectedConstruction == item; - } - - private Character SpawnMonster(string speciesName, Vector2 pos) - { - var character = Character.Create(speciesName, pos, ToolBox.RandomSeed(8)); - var ai = character.AIController as EnemyAIController; - ai.TargetOutposts = true; - character.CharacterHealth.SetVitality(character.Health / 2); - character.AnimController.Limbs.Where(l => l.attack != null).Select(l => l.attack).ForEach(a => a.AfterAttack = AIBehaviorAfterAttack.FallBack); - return character; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs deleted file mode 100644 index 7d72179b1..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ /dev/null @@ -1,307 +0,0 @@ -using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.Tutorials -{ - abstract class ScenarioTutorial : Tutorial - { - private CoroutineHandle tutorialCoroutine; - - private Character character; - - private const string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub"; - private const string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub"; - //private const string endOutpostPath = ""; - - private const string levelSeed = "nLoZLLtza"; - private const string levelParams = "ColdCavernsTutorial"; - - //private const string spawnSub = "startoutpost"; - private const SpawnType spawnPointType = SpawnType.Human; - - private SubmarineInfo startOutpost = null; - private SubmarineInfo endOutpost = null; - private bool currentTutorialCompleted = false; - private float fadeOutTime = 3f; - protected float waitBeforeFade = 4f; - - // Colors - protected Color highlightColor = Color.OrangeRed; - protected Color uiHighlightColor = new Color(150, 50, 0); - protected Color buttonHighlightColor = new Color(255, 100, 0); - protected Color inaccessibleColor = GUIStyle.Red; - protected Color accessibleColor = GUIStyle.Green; - - protected ScenarioTutorial(Identifier identifier, params Segment[] segments) : base(identifier, segments) { } - - protected abstract void Initialize(); - - protected override IEnumerable Loading() - { - SubmarineInfo subInfo = new SubmarineInfo(submarinePath); - - LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier == levelParams); - - yield return CoroutineStatus.Running; - - GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); - (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; - - if (generationParams != null) - { - Biome biome = - Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? - Biome.Prefabs.First(); - - if (!string.IsNullOrEmpty(startOutpostPath)) - { - startOutpost = new SubmarineInfo(startOutpostPath); - } - - /*if (!string.IsNullOrEmpty(endOutpostPath)) - { - endOutpost = new SubmarineInfo(endOutpostPath); - }*/ - - LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome); - GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost, endOutpost: endOutpost); - } - else - { - GameMain.GameSession.StartRound(levelSeed); - } - - GameMain.GameSession.EventManager.ActiveEvents.Clear(); - GameMain.GameSession.EventManager.Enabled = false; - GameMain.GameScreen.Select(); - - - Submarine.MainSub.GodMode = true; - foreach (Structure wall in Structure.WallList) - { - if (wall.Submarine != null && wall.Submarine.Info.IsOutpost) - { - wall.Indestructible = true; - } - } - - CharacterInfo charInfo = GetCharacterInfo(); - - WayPoint wayPoint = GetSpawnPoint(charInfo); - - if (wayPoint == null) - { - DebugConsole.ThrowError("A waypoint with the spawntype \"" + spawnPointType + "\" is required for the tutorial event"); - yield return CoroutineStatus.Failure; - yield break; - } - - character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); - character.TeamID = CharacterTeamType.Team1; - Character.Controlled = character; - character.GiveJobItems(null); - - var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier()); - if (idCard == null) - { - DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); - yield return CoroutineStatus.Failure; - yield break; - } - idCard.AddTag("com"); - idCard.AddTag("eng"); - - foreach (Item item in Item.ItemList) - { - Door door = item.GetComponent(); - if (door != null) - { - door.CanBeWelded = false; - } - } - - tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); - - Initialize(); - - yield return CoroutineStatus.Success; - } - - protected abstract CharacterInfo GetCharacterInfo(); - - public override void AddToGUIUpdateList() - { - if (!currentTutorialCompleted) - { - base.AddToGUIUpdateList(); - } - } - - private WayPoint GetSpawnPoint(CharacterInfo charInfo) - { - /*Submarine spawnSub = null; - - if (this.spawnSub != string.Empty) - { - switch (this.spawnSub) - { - case "startoutpost": - spawnSub = Level.Loaded.StartOutpost; - break; - - case "endoutpost": - spawnSub = Level.Loaded.EndOutpost; - break; - - default: - spawnSub = Submarine.MainSub; - break; - } - }*/ - Submarine spawnSub = Level.Loaded.StartOutpost; - return WayPoint.GetRandom(spawnPointType, charInfo.Job?.Prefab, spawnSub); - } - - protected bool HasOrder(Character character, string identifier, string option = null) - { - var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Identifier == identifier) - { - if (option == null) - { - return true; - } - else - { - return currentOrderInfo?.Option == option; - } - } - - return false; - } - - protected void SetHighlight(Item item, bool state) - { - if (item.ExternalHighlight == state) return; - item.SpriteColor = (state) ? highlightColor : Color.White; - item.ExternalHighlight = state; - } - - protected void SetHighlight(Structure structure, bool state) - { - structure.SpriteColor = (state) ? highlightColor : Color.White; - structure.ExternalHighlight = state; - } - - protected void SetHighlight(Character character, bool state) - { - character.ExternalHighlight = state; - } - - protected void SetDoorAccess(Door door, LightComponent light, bool state) - { - if (state && door != null) door.requiredItems.Clear(); - if (light != null) light.LightColor = (state) ? accessibleColor : inaccessibleColor; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - if (character != null) - { - if (character.Oxygen < 1) - { - character.Oxygen = 1; - } - if (character.IsDead) - { - CoroutineManager.StartCoroutine(Dead()); - } - else if (Character.Controlled == null) - { - if (tutorialCoroutine != null) - { - CoroutineManager.StopCoroutines(tutorialCoroutine); - } - GUI.PreventPauseMenuToggle = false; - ContentRunning = false; - infoBox = null; - } - else if (Character.Controlled.IsDead) - { - CoroutineManager.StartCoroutine(Dead()); - } - } - } - - public override void Stop() - { - if (tutorialCoroutine != null) - { - CoroutineManager.StopCoroutines(tutorialCoroutine); - } - base.Stop(); - } - - private IEnumerable Dead() - { - GUI.PreventPauseMenuToggle = true; - Character.Controlled = character = null; - Stop(); - - GameAnalyticsManager.AddDesignEvent("Tutorial:Died"); - - yield return new WaitForSeconds(3.0f); - - var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - messageBox.Buttons[0].OnClicked += Restart; - messageBox.Buttons[0].OnClicked += messageBox.Close; - - - messageBox.Buttons[1].OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu; - messageBox.Buttons[1].OnClicked += messageBox.Close; - - yield return CoroutineStatus.Success; - } - - protected IEnumerable TutorialCompleted() - { - GUI.PreventPauseMenuToggle = true; - - Character.Controlled.ClearInputs(); - Character.Controlled = null; - - GameAnalyticsManager.AddDesignEvent("Tutorial:Completed"); - - yield return new WaitForSeconds(waitBeforeFade); - - var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: fadeOutTime); - currentTutorialCompleted = Completed = true; - while (endCinematic.Running) yield return null; - Stop(); - GameMain.MainMenuScreen.ReturnToMainMenu(null, null); - } - - protected void Heal(Character character) - { - character.SetAllDamage(0.0f, 0.0f, 0.0f); - character.Oxygen = 100.0f; - character.Bloodloss = 0.0f; - character.SetStun(0.0f, true); - } - - protected Item FindOrGiveItem(Character character, Identifier identifier) - { - var item = character.Inventory.FindItemByIdentifier(identifier); - if (item != null && !item.Removed) { return item; } - - ItemPrefab itemPrefab = MapEntityPrefab.Find(name: null, identifier: identifier) as ItemPrefab; - item = new Item(itemPrefab, Vector2.Zero, submarine: null); - character.Inventory.TryPutItem(item, character, item.AllowedSlots); - return item; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 42795e0bf..a9c7f14e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,104 +1,140 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Reflection; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Items.Components; -using Barotrauma.Extensions; -using System.Collections.Immutable; namespace Barotrauma.Tutorials { - enum TutorialContentType { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; + enum AutoPlayVideo { Yes, No }; - /// - /// If you're seeing this and are currently working on improving the tutorials, consider - /// deleting this class and all that derive from it, and starting from scratch. - /// - abstract class Tutorial + enum TutorialSegmentType { MessageBox, InfoBox, Objective }; + + sealed class Tutorial { #region Constants - public const string PlayableContentPath = "Content/Tutorials/TutorialVideos/"; + + private const SpawnType SpawnPointType = SpawnType.Human; + private const float FadeOutTime = 3f; + private const float WaitBeforeFade = 4f; + #endregion #region Tutorial variables - public static ImmutableHashSet Types; - static Tutorial() - { - Types = ReflectionUtils.GetDerivedNonAbstract() - .ToImmutableHashSet(); - } public readonly Identifier Identifier; public LocalizedString DisplayName { get; } - public bool ContentRunning { get; protected set; } + public bool ContentRunning { get; private set; } - protected GUIComponent infoBox; + private GUIComponent infoBox; private Action infoBoxClosedCallback; - protected VideoPlayer videoPlayer; - protected Point screenResolution; - protected WindowMode windowMode; - protected float prevUIScale; + private VideoPlayer videoPlayer; + private Point screenResolution; + private WindowMode windowMode; + private float prevUIScale; - private GUIFrame holderFrame, objectiveFrame; - private readonly List activeObjectives; - private readonly LocalizedString objectiveTranslated; + private GUILayoutGroup objectiveGroup; + private readonly LocalizedString objectiveTextTranslated; - protected readonly ImmutableArray segments; - protected Index activeContentSegmentIndex; - protected Segment activeContentSegment => segments[activeContentSegmentIndex]; + private readonly List ActiveObjectives = new List(); + private const float ObjectiveComponentAnimationTime = 1.5f; + private Segment ActiveContentSegment { get; set; } - protected class Segment + public class Segment { - public struct Text + public readonly record struct Text( + Identifier Tag, + int Width = DefaultWidth, + int Height = DefaultHeight, + Anchor Anchor = Anchor.Center); + + public readonly record struct Video( + string FullPath, + Identifier TextTag, + int Width = DefaultWidth, + int Height = DefaultHeight) { - public Identifier Tag; - public int Width; - public int Height; - public Anchor Anchor; + public string FileName => Path.GetFileName(FullPath.CleanUpPath()); + public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); } - public struct Video - { - public string File; - public Identifier TextTag; - public int Width; - public int Height; - } + private const int DefaultWidth = 450; + private const int DefaultHeight = 80; - public bool IsTriggered; - public GUIButton ReplayButton; - public GUITextBlock LinkedTitle, LinkedText; - public object[] Args; - public LocalizedString Objective; + public GUIImage ObjectiveStateIndicator; + public GUIButton ObjectiveButton; + public GUITextBlock LinkedTextBlock; + public LocalizedString ObjectiveText; public readonly Identifier Id; - public readonly Text? TextContent; - public readonly Video? VideoContent; - public readonly TutorialContentType ContentType; + public readonly Text TextContent; + public readonly Video VideoContent; + public readonly AutoPlayVideo AutoPlayVideo; - public Segment(Identifier id, Identifier objectiveTextTag, TutorialContentType contentType, Text? textContent = null, Video? videoContent = null) + public Action OnClickObjective; + + public TutorialSegmentType SegmentType { get; private set; } + + public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); + } + + public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + return new Segment(id, objectiveTextTag, onClickObjective); + } + + public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) + { + return new Segment(id, objectiveTextTag); + } + + private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { Id = id; - Objective = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - ContentType = contentType; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + AutoPlayVideo = autoPlayVideo; TextContent = textContent; VideoContent = videoContent; - - IsTriggered = false; + SegmentType = TutorialSegmentType.InfoBox; } - } + + private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + OnClickObjective = onClickObjective; + SegmentType = TutorialSegmentType.MessageBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + SegmentType = TutorialSegmentType.Objective; + } + + public void ConnectMessageBox(Segment messageBoxSegment) + { + SegmentType = TutorialSegmentType.MessageBox; + OnClickObjective = messageBoxSegment.OnClickObjective; + } + } private bool completed; public bool Completed { - get { return completed; } - protected set + get + { + return completed; + } + private set { if (completed == value) { return; } completed = value; @@ -109,26 +145,149 @@ namespace Barotrauma.Tutorials GameSettings.SaveCurrentConfig(); } } + + public readonly TutorialPrefab TutorialPrefab; + private readonly EventPrefab eventPrefab; + + private CoroutineHandle tutorialCoroutine; + private CoroutineHandle completedCoroutine; + + private Character character; + + private string SubmarinePath => TutorialPrefab.SubmarinePath.Value; + private string StartOutpostPath => TutorialPrefab.OutpostPath.Value; + private string LevelSeed => TutorialPrefab.LevelSeed; + private string LevelParams => TutorialPrefab.LevelParams; + + private SubmarineInfo startOutpost = null; + + public readonly List<(Entity entity, Identifier iconStyle)> Icons = new List<(Entity entity, Identifier iconStyle)>(); + #endregion #region Tutorial Controls - protected Tutorial(Identifier identifier, params Segment[] segments) + + public Tutorial(TutorialPrefab prefab) { - Identifier = identifier; - this.segments = segments.ToImmutableArray(); + Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier(); DisplayName = TextManager.Get(Identifier); - activeObjectives = new List(); - objectiveTranslated = TextManager.Get("Tutorial.Objective"); + objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); + + TutorialPrefab = prefab; + eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); } - protected abstract IEnumerable Loading(); + private IEnumerable Loading() + { + SubmarineInfo subInfo = new SubmarineInfo(SubmarinePath); + + LevelGenerationParams.LevelParams.TryGet(LevelParams, out LevelGenerationParams generationParams); + + yield return CoroutineStatus.Running; + + GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); + (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; + + if (generationParams is not null) + { + Biome biome = + Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? + Biome.Prefabs.First(); + + if (!string.IsNullOrEmpty(StartOutpostPath)) + { + startOutpost = new SubmarineInfo(StartOutpostPath); + } + + LevelData tutorialLevel = new LevelData(LevelSeed, 0, 0, generationParams, biome); + GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost); + } + else + { + GameMain.GameSession.StartRound(LevelSeed); + } + + GameMain.GameSession.EventManager.ActiveEvents.Clear(); + GameMain.GameSession.EventManager.Enabled = true; + GameMain.GameScreen.Select(); + + if (Submarine.MainSub != null) + { + Submarine.MainSub.GodMode = true; + } + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != null && wall.Submarine.Info.IsOutpost) + { + wall.Indestructible = true; + } + } + + var charInfo = TutorialPrefab.GetTutorialCharacterInfo(); + + var wayPoint = WayPoint.GetRandom(SpawnPointType, charInfo.Job?.Prefab, Level.Loaded.StartOutpost); + + if (wayPoint == null) + { + DebugConsole.ThrowError("A waypoint with the spawntype \"" + SpawnPointType + "\" is required for the tutorial event"); + yield return CoroutineStatus.Failure; + yield break; + } + + character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); + character.TeamID = CharacterTeamType.Team1; + Character.Controlled = character; + character.GiveJobItems(null); + + var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier()); + if (idCard == null) + { + DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); + yield return CoroutineStatus.Failure; + yield break; + } + idCard.AddTag("com"); + idCard.AddTag("eng"); + + foreach (Item item in Item.ItemList) + { + Door door = item.GetComponent(); + if (door != null) + { + door.CanBeWelded = false; + } + } + + tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); + + Initialize(); + + yield return CoroutineStatus.Success; + } + + private void Initialize() + { + GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; + GameMain.GameSession.CrewManager.AutoHideCrewList(); + + if (Character.Controlled is Character character) + { + foreach (Item item in character.Inventory.AllItemsMod) + { + if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } + item.Unequip(character); + character.Inventory.RemoveItem(item); + } + } + } public void Start() { videoPlayer = new VideoPlayer(); GameMain.Instance.ShowLoading(Loading()); - - activeObjectives.Clear(); + ActiveObjectives.Clear(); + ActiveContentSegment = null; + CreateObjectiveFrame(); // Setup doors: Clear all requirements, unless the door is setup as locked. @@ -145,35 +304,76 @@ namespace Barotrauma.Tutorials } } - public virtual void AddToGUIUpdateList() + public void AddToGUIUpdateList() { if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) { CreateObjectiveFrame(); } - - if (objectiveFrame != null && activeObjectives.Count > 0) + if (ActiveObjectives.Count > 0) { - objectiveFrame.AddToGUIUpdateList(order: -1); + objectiveGroup?.AddToGUIUpdateList(order: -1); } - - if (infoBox != null) infoBox.AddToGUIUpdateList(order: 100); - if (videoPlayer != null) videoPlayer.AddToGUIUpdateList(order: 100); + infoBox?.AddToGUIUpdateList(order: 100); + videoPlayer?.AddToGUIUpdateList(order: 100); } - public virtual void Update(float deltaTime) + public void Update() { videoPlayer?.Update(); - if (activeObjectives != null) + if (character != null) { - for (int i = 0; i < activeObjectives.Count; i++) + if (character.Oxygen < 1) { - CheckActiveObjectives(activeObjectives[i], deltaTime); + character.Oxygen = 1; + } + if (character.IsDead) + { + CoroutineManager.StartCoroutine(Dead()); + } + else if (Character.Controlled == null) + { + if (tutorialCoroutine != null) + { + CoroutineManager.StopCoroutines(tutorialCoroutine); + } + if (completedCoroutine == null && !CoroutineManager.IsCoroutineRunning(completedCoroutine)) + { + GUI.PreventPauseMenuToggle = false; + } + ContentRunning = false; + infoBox = null; + } + else + { + character = Character.Controlled; } } } + private IEnumerable Dead() + { + GUI.PreventPauseMenuToggle = true; + Character.Controlled = character = null; + Stop(); + + GameAnalyticsManager.AddDesignEvent("Tutorial:Died"); + + yield return new WaitForSeconds(3.0f); + + var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + messageBox.Buttons[0].OnClicked += Restart; + messageBox.Buttons[0].OnClicked += messageBox.Close; + + + messageBox.Buttons[1].OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu; + messageBox.Buttons[1].OnClicked += messageBox.Close; + + yield return CoroutineStatus.Success; + } + public void CloseActiveContentGUI() { if (videoPlayer.IsPlaying) @@ -182,257 +382,338 @@ namespace Barotrauma.Tutorials } else if (infoBox != null) { - CloseInfoFrame(null, null); + CloseInfoFrame(); } } - public virtual IEnumerable UpdateState() + public IEnumerable UpdateState() { - yield return CoroutineStatus.Success; - } - - protected bool Restart(GUIButton button, object obj) - { - GUI.PreventPauseMenuToggle = false; - return true; - } - - protected virtual void TriggerTutorialSegment(Index index, params object[] args) - { - Inventory.DraggingItems.Clear(); - ContentRunning = true; - activeContentSegmentIndex = index; - segments[index].Args = args; - - LocalizedString tutorialText = TextManager.GetFormatted(segments[index].TextContent.Value.Tag, args); - tutorialText = TextManager.ParseInputTypes(tutorialText); - LocalizedString objectiveText = string.Empty; - - if (!segments[index].Objective.IsNullOrEmpty()) + while (GameMain.Instance.LoadingScreenOpen || Level.Loaded == null || Level.Loaded.Generating) { - if (args.Length == 0) + yield return new WaitForSeconds(0.1f); + } + + if (eventPrefab == null) + { + DebugConsole.LogError($"No tutorial event defined for the tutorial (identifier: \"{TutorialPrefab?.Identifier.ToString() ?? "null"})\""); + yield return CoroutineStatus.Failure; + } + + if (eventPrefab.CreateInstance() is Event eventInstance) + { + GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventInstance); + while (!eventInstance.IsFinished) { - objectiveText = segments[index].Objective; + yield return CoroutineStatus.Running; } - else - { - objectiveText = TextManager.GetFormatted(segments[index].Objective, args); - } - objectiveText = TextManager.ParseInputTypes(objectiveText); - segments[index].Objective = objectiveText; } else { - segments[index].IsTriggered = true; // Complete at this stage only if no related objective + DebugConsole.LogError($"Failed to create an instance for a tutorial event (identifier: \"{eventPrefab.Identifier}\""); + yield return CoroutineStatus.Failure; } + yield return CoroutineStatus.Success; + } - switch (segments[index].ContentType) + public void Complete() + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:Completed"); + completedCoroutine = CoroutineManager.StartCoroutine(TutorialCompleted()); + + IEnumerable TutorialCompleted() { - case TutorialContentType.None: + while (GUI.PauseMenuOpen) { yield return CoroutineStatus.Running; } + + GUI.PreventPauseMenuToggle = true; + Character.Controlled.ClearInputs(); + Character.Controlled = null; + GameAnalyticsManager.AddDesignEvent("Tutorial:Completed"); + + yield return new WaitForSeconds(WaitBeforeFade); + + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: FadeOutTime); + Completed = true; + + while (endCinematic.Running) { yield return CoroutineStatus.Running; } + + Stop(); + GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + } + } + + private bool Restart(GUIButton button, object obj) + { + GUIMessageBox.MessageBoxes.Clear(); + GameMain.MainMenuScreen.ReturnToMainMenu(button, obj); + Start(); + return true; + } + + public void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + { + if (segment.SegmentType != TutorialSegmentType.InfoBox) + { + ActiveObjectives.Add(segment); + AddToObjectiveList(segment, connectObjective); + return; + } + + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + + var title = TextManager.Get(segment.Id); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); + tutorialText = TextManager.ParseInputTypes(tutorialText); + + switch (segment.AutoPlayVideo) + { + case AutoPlayVideo.Yes: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: LoadActiveContentVideo); break; - case TutorialContentType.Video: - infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.Value.Width, - activeContentSegment.TextContent.Value.Height, - activeContentSegment.TextContent.Value.Anchor, true, () => LoadVideo(activeContentSegment)); - break; - case TutorialContentType.ManualVideo: - infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.Value.Width, - activeContentSegment.TextContent.Value.Height, - activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment)); - break; - case TutorialContentType.TextOnly: - infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.Value.Width, - activeContentSegment.TextContent.Value.Height, - activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment); + case AutoPlayVideo.No: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: StopCurrentContentSegment, + onVideoButtonClicked: LoadActiveContentVideo); break; } } - public virtual void Stop() + public void CompleteTutorialSegment(Identifier segmentId) { + if (GetActiveObjective(segmentId) is not Segment segment) + { + DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); + return; + } + if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + { + //return if already completed + if (segment.ObjectiveStateIndicator.Style == style) { return; } + segment.ObjectiveStateIndicator.ApplyStyle(style); + } + segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:{segmentId}:Completed"); + } + + public void RemoveTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) + { + DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); + return; + } + segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); + segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); + var parent = segment.LinkedTextBlock.Parent; + parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => + { + ActiveObjectives.Remove(segment); + objectiveGroup?.Recalculate(); + }); + parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + } + + private Segment GetActiveObjective(Identifier id) => ActiveObjectives.FirstOrDefault(s => s.Id == id); + + public void Stop() + { + if (tutorialCoroutine != null) + { + CoroutineManager.StopCoroutines(tutorialCoroutine); + } ContentRunning = false; infoBox = null; - videoPlayer.Remove(); + videoPlayer?.Remove(); } + #endregion #region Objectives + + /// + /// Create the objective list that holds the objectives (called on start and on resolution change) + /// private void CreateObjectiveFrame() { - holderFrame = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center)); - objectiveFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ObjectiveAnchor, holderFrame.RectTransform), style: null); - - for (int i = 0; i < activeObjectives.Count; i++) + var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null); + objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) { - CreateObjectiveGUI(activeObjectives[i], i, segments[activeObjectives[i]].ContentType); + AbsoluteSpacing = (int)GUIStyle.Font.LineHeight + }; + for (int i = 0; i < ActiveObjectives.Count; i++) + { + AddToObjectiveList(ActiveObjectives[i]); } - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; prevUIScale = GUI.Scale; } - protected void StopCurrentContentSegment() + /// + /// Stops content running and adds the active segment to the objective list + /// + private void StopCurrentContentSegment() { - if (!activeContentSegment.Objective.IsNullOrEmpty()) + if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) { - AddNewObjective(activeContentSegmentIndex, activeContentSegment.ContentType); + ActiveObjectives.Add(ActiveContentSegment); + AddToObjectiveList(ActiveContentSegment); + } + ContentRunning = false; + ActiveContentSegment = null; + } + + /// + /// Adds the segment to the objective list + /// + private void AddToObjectiveList(Segment segment, bool connectExisting = false) + { + if (connectExisting) + { + if (ActiveObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) + { + existingSegment.ConnectMessageBox(segment); + SetButtonBehavior(existingSegment); + } + return; } - ContentRunning = false; - activeContentSegmentIndex = Index.End; - } - - protected virtual void CheckActiveObjectives(Index objective, float deltaTime) - { - - } - - protected bool HasObjective(Index segment) - { - return activeObjectives.Contains(segment); - } - - protected void AddNewObjective(Index segment, TutorialContentType type) - { - activeObjectives.Add(segment); - CreateObjectiveGUI(segment, activeObjectives.Count - 1, type); - } - - private void CreateObjectiveGUI(Index segmentIndex, int index, TutorialContentType type) - { - var segment = segments[segmentIndex]; - LocalizedString objectiveText = TextManager.ParseInputTypes(segment.Objective); - Point replayButtonSize = new Point((int)(GUIStyle.LargeFont.MeasureString(objectiveText).X), (int)(GUIStyle.LargeFont.MeasureString(objectiveText).Y * 1.45f)); - - segment.ReplayButton = new GUIButton(new RectTransform(replayButtonSize, objectiveFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(0, (replayButtonSize.Y + (int)(20f * GUI.Scale)) * index) }, style: null); - segment.ReplayButton.OnClicked += (GUIButton btn, object userdata) => + var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) { - if (type == TutorialContentType.Video) - { - ReplaySegmentVideo(segment); - } - else - { - ShowSegmentText(segment); - } - return true; + AbsoluteOffset = GetObjectiveHiddenPosition(), + MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) }; - - LocalizedString objectiveTitleText = TextManager.ParseInputTypes(objectiveTranslated); - int yOffset = (int)((GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).Y + 5)); - segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.BottomLeft) /*{ AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }*/, - objectiveTitleText, textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) + var frame = new GUIFrame(frameRt, style: null) { - ForceUpperCase = ForceUpperCase.Yes + CanBeFocused = true }; + objectiveGroup.Recalculate(); - segment.LinkedText = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.LargeFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.TopLeft) /*{ AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }*/, - objectiveText, textColor: new Color(4, 180, 108), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft); - - segment.LinkedTitle.Color = segment.LinkedTitle.HoverColor = segment.LinkedTitle.PressedColor = segment.LinkedTitle.SelectedColor = Color.Transparent; - segment.LinkedText.Color = segment.LinkedText.HoverColor = segment.LinkedText.PressedColor = segment.LinkedText.SelectedColor = Color.Transparent; - segment.ReplayButton.Color = segment.ReplayButton.HoverColor = segment.ReplayButton.PressedColor = segment.ReplayButton.SelectedColor = Color.Transparent; + segment.LinkedTextBlock = new GUITextBlock( + new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), + TextManager.ParseInputTypes(segment.ObjectiveText), + wrap: true); + + var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); + segment.LinkedTextBlock.RectTransform.NonScaledSize = size; + segment.LinkedTextBlock.RectTransform.MinSize = size; + segment.LinkedTextBlock.RectTransform.MaxSize = size; + segment.LinkedTextBlock.RectTransform.IsFixedSize = true; + frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); + frame.RectTransform.IsFixedSize = true; + + var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); + segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); + + SetTransparent(segment.LinkedTextBlock); + + segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) + { + ToolTip = objectiveTextTranslated + }; + SetButtonBehavior(segment); + SetTransparent(segment.ObjectiveButton); + + frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); + + static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; + + void SetButtonBehavior(Segment segment) + { + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => + { + if (segment.SegmentType == TutorialSegmentType.InfoBox) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + ReplaySegmentVideo(segment); + } + else + { + ShowSegmentText(segment); + } + } + else if (segment.SegmentType == TutorialSegmentType.MessageBox) + { + segment.OnClickObjective?.Invoke(); + } + return true; + }; + } } private void ReplaySegmentVideo(Segment segment) { - if (ContentRunning) return; + if (ContentRunning) { return; } Inventory.DraggingItems.Clear(); ContentRunning = true; LoadVideo(segment); - //videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); } private void ShowSegmentText(Segment segment) { - if (ContentRunning) return; + if (ContentRunning) { return; } Inventory.DraggingItems.Clear(); ContentRunning = true; - - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Value.Tag, segment.Args); - - Action videoAction = null; - - if (segment.ContentType != TutorialContentType.TextOnly) - { - videoAction = () => LoadVideo(segment); - } - - infoBox = CreateInfoFrame(TextManager.Get(segment.Id), tutorialText, - segment.TextContent.Value.Width, - segment.TextContent.Value.Height, - segment.TextContent.Value.Anchor, true, () => ContentRunning = false, videoAction); + ActiveContentSegment = segment; + infoBox = CreateInfoFrame( + TextManager.Get(segment.Id), + TextManager.Get(segment.TextContent.Tag), + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: () => ContentRunning = false, + onVideoButtonClicked: () => LoadVideo(segment)); } - protected void RemoveCompletedObjective(Index segmentIndex) + private Point GetObjectiveHiddenPosition(RectTransform rt = null) { - if (!HasObjective(segmentIndex)) return; - var segment = segments[segmentIndex]; - segment.IsTriggered = true; - segment.ReplayButton.OnClicked = null; - - int checkMarkHeight = (int)(segment.ReplayButton.Rect.Height * 1.2f); - int checkMarkWidth = (int)(checkMarkHeight * 0.93f); - - Color color = new Color(4, 180, 108); - - int objectiveTextWidth = segment.LinkedText.Rect.Width; - int objectiveTitleWidth = segment.LinkedTitle.Rect.Width; - - RectTransform rectTA; - if (objectiveTextWidth > objectiveTitleWidth) - { - rectTA = new RectTransform(new Point(checkMarkWidth, checkMarkHeight), segment.ReplayButton.RectTransform, Anchor.BottomRight, Pivot.BottomRight); - rectTA.AbsoluteOffset = new Point(-rectTA.Rect.Width - (int)(25 * GUI.Scale), 0); - } - else - { - rectTA = new RectTransform(new Point(checkMarkWidth, checkMarkHeight), segment.ReplayButton.RectTransform, Anchor.BottomRight, Pivot.BottomRight); - rectTA.AbsoluteOffset = new Point(-rectTA.Rect.Width - (int)(25 * GUI.Scale) - (objectiveTitleWidth - objectiveTextWidth), 0); - } - - GUIImage checkmark = new GUIImage(rectTA, "CheckMark"); - checkmark.Color = checkmark.SelectedColor = checkmark.HoverColor = checkmark.PressedColor = color; - - RectTransform rectTB = new RectTransform(new Vector2(1.0f, .8f), segment.LinkedText.RectTransform, Anchor.Center, Pivot.Center); - GUIImage stroke = new GUIImage(rectTB, "Stroke"); - stroke.Color = stroke.SelectedColor = stroke.HoverColor = stroke.PressedColor = color; - - CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segmentIndex)); - } - - private IEnumerable WaitForObjectiveEnd(Index objectiveIndex) - { - var objective = segments[objectiveIndex]; - yield return new WaitForSeconds(2.0f); - objectiveFrame.RemoveChild(objective.ReplayButton); - activeObjectives.Remove(objectiveIndex); - - for (int i = 0; i < activeObjectives.Count; i++) - { - var activeObjective = segments[activeObjectives[i]]; - activeObjective.ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjective.ReplayButton.Rect.Height + 20) * i); - } + return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); } #endregion #region InfoFrame - protected bool CloseInfoFrame(GUIButton button, object userData) + + private void CloseInfoFrame() => CloseInfoFrame(null, null); + + private bool CloseInfoFrame(GUIButton button, object userData) { infoBox = null; infoBoxClosedCallback?.Invoke(); return true; } - protected GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action callback = null, Action showVideo = null) + /// + // Creates and displays a tutorial info box + /// + private GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) { - if (hasButton) height += 60; + if (hasButton) + { + height += 60; + } width = (int)(width * GUI.Scale); height = (int)(height * GUI.Scale); @@ -467,7 +748,7 @@ namespace Barotrauma.Tutorials GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); textBlock.RectTransform.IsFixedSize = true; - infoBoxClosedCallback = callback; + infoBoxClosedCallback = onInfoBoxClosed; if (hasButton) { @@ -477,7 +758,7 @@ namespace Barotrauma.Tutorials }; buttonContainer.RectTransform.IsFixedSize = true; - if (showVideo != null) + if (onVideoButtonClicked != null) { buttonContainer.Stretch = true; var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), @@ -485,7 +766,7 @@ namespace Barotrauma.Tutorials { OnClicked = (GUIButton button, object obj) => { - showVideo(); + onVideoButtonClicked(); return true; } }; @@ -509,57 +790,39 @@ namespace Barotrauma.Tutorials return background; } + #endregion #region Video - protected void LoadVideo(Segment segment) + + private void LoadVideo(Segment segment) { - if (videoPlayer == null) videoPlayer = new VideoPlayer(); - if (segment.ContentType != TutorialContentType.ManualVideo) + videoPlayer ??= new VideoPlayer(); + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) { videoPlayer.LoadContent( - PlayableContentPath, - new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), - new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width), - segment.Id, true, segment.Objective, StopCurrentContentSegment); + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), + contentId: segment.Id, + startPlayback: true, + objective: segment.ObjectiveText, + onStop: StopCurrentContentSegment); } else { - videoPlayer.LoadContent(PlayableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), null, segment.Id, true, string.Empty, null); - } - } - #endregion - - #region Highlights - protected void HighlightInventorySlot(Inventory inventory, Identifier identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) - { - if (inventory.visualSlots == null) { return; } - for (int i = 0; i < inventory.Capacity; i++) - { - if (inventory.GetItemAt(i)?.Prefab.Identifier == identifier) - { - HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); - } + videoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: null, + contentId: segment.Id, + startPlayback: true, + objective: string.Empty); } } - protected void HighlightInventorySlotWithTag(Inventory inventory, Identifier tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) - { - if (inventory.visualSlots == null) { return; } - for (int i = 0; i < inventory.Capacity; i++) - { - if (inventory.GetItemAt(i)?.HasTag(tag) ?? false) - { - HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); - } - } - } + private void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); - protected void HighlightInventorySlot(Inventory inventory, int index, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) - { - if (inventory.visualSlots == null || index < 0 || inventory.visualSlots[index].HighlightTimer > 0) { return; } - inventory.visualSlots[index].ShowBorderHighlight(color, fadeInDuration, fadeOutDuration, scaleUpAmount); - } #endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index 2b904f82f..d155a1c23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -6,10 +6,7 @@ namespace Barotrauma { public Tutorial Tutorial; - public TutorialMode(GameModePreset preset) - : base(preset) - { - } + public TutorialMode(GameModePreset preset) : base(preset) { } public override void Start() { @@ -31,7 +28,7 @@ namespace Barotrauma public override void Update(float deltaTime) { base.Update(deltaTime); - Tutorial.Update(deltaTime); + Tutorial.Update(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 627f79236..dd566ee34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -14,6 +14,7 @@ namespace Barotrauma public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; + private float prevHudScale; private TabMenu tabMenu; @@ -119,6 +120,7 @@ namespace Barotrauma return true; } }; + prevHudScale = GameSettings.CurrentConfig.Graphics.HUDScale; } public void AddToGUIUpdateList() @@ -178,6 +180,12 @@ namespace Barotrauma } } + public void HUDScaleChanged() + { + CreateTopLeftButtons(); + GameMode?.HUDScaleChanged(); + } + partial void UpdateProjSpecific(float deltaTime) { if (GUI.DisableHUD) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index a7ee9cc4c..0dfc29ed7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -112,19 +112,19 @@ namespace Barotrauma CheckReminders(); } - public static void OnSetSelectedConstruction(Character character, Item oldConstruction, Item newConstruction) + public static void OnSetSelectedItem(Character character, Item oldItem, Item newItem) { - if (oldConstruction == newConstruction) { return; } + if (oldItem == newItem) { return; } - if (Character.Controlled != null && Character.Controlled == character && oldConstruction != null && oldConstruction.GetComponent() == null) + if (Character.Controlled != null && Character.Controlled == character && oldItem != null && !oldItem.IsLadder) { TimeStoppedInteracting = Timing.TotalTime; } - if (newConstruction == null) { return; } - if (newConstruction.GetComponent() != null) { return; } - if (newConstruction.GetComponent() is ConnectionPanel cp && cp.User == character) { return; } - OnStartedInteracting(character, newConstruction); + if (newItem == null) { return; } + if (newItem.IsLadder) { return; } + if (newItem.GetComponent() is ConnectionPanel cp && cp.User == character) { return; } + OnStartedInteracting(character, newItem); } private static void OnStartedInteracting(Character character, Item item) @@ -177,10 +177,10 @@ namespace Barotrauma private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } - if (Character.Controlled?.SelectedConstruction == null) { return; } + if (Character.Controlled?.SelectedItem == null) { return; } - if (Character.Controlled.SelectedConstruction.GetComponent() is Reactor reactor && reactor.PowerOn && - Character.Controlled.SelectedConstruction.OwnInventory?.AllItems is IEnumerable containedItems && + if (Character.Controlled.SelectedItem.GetComponent() is Reactor reactor && reactor.PowerOn && + Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable containedItems && containedItems.Count(i => i.HasTag("reactorfuel")) > 1) { if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } @@ -272,7 +272,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (sonar == null || sonar.Removed) { return; } if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } - if (Character.Controlled.SelectedConstruction != sonar) { return; } + if (Character.Controlled.SelectedItem != sonar) { return; } if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } DisplayHint("onsonarspottedenemy".ToIdentifier()); } @@ -305,7 +305,7 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } - if (character.SelectedConstruction != null || character.FocusedItem != null) { return; } + if (character.HasSelectedAnyItem || character.FocusedItem != null) { return; } if (item == null || !item.IsShootable || !item.RequireAimToUse) { return; } if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } if (GUI.MouseOn != null) { return; } @@ -317,7 +317,7 @@ namespace Barotrauma variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim)) }, onUpdate: () => { - if (character.SelectedConstruction == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) + if (character.SelectedItem == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) { ActiveHintMessageBox.Close(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 0811b26ce..c06efe8d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -354,14 +354,14 @@ namespace Barotrauma private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); - writeMessage.Write((byte)ClientPacketHeader.MEDICAL); + writeMessage.WriteByte((byte)ClientPacketHeader.MEDICAL); return writeMessage; } private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod) { IWriteMessage msg = StartSending(); - msg.Write((byte)header); + msg.WriteByte((byte)header); netStruct?.Write(msg); GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 4db03f290..eed8abac4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -83,7 +83,7 @@ namespace Barotrauma foreach (var (id, _) in Clients) { - Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.ID == id); + Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.SessionId == id); GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id }; GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true }; @@ -93,7 +93,7 @@ namespace Barotrauma if (client == null) { - string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.ID}: {c.Name}\n"); + string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); DebugConsole.ThrowError($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); } @@ -139,9 +139,9 @@ namespace Barotrauma public static void ClientRead(IReadMessage inc) { - ReadyCheckState state = (ReadyCheckState) inc.ReadByte(); + ReadyCheckState state = (ReadyCheckState)inc.ReadByte(); CrewManager? crewManager = GameMain.GameSession?.CrewManager; - List otherClients = GameMain.Client.ConnectedClients; + var otherClients = GameMain.Client.ConnectedClients; if (crewManager == null || otherClients == null) { if (state == ReadyCheckState.Start) @@ -165,7 +165,7 @@ namespace Barotrauma if (hasAuthor) { authorId = inc.ReadByte(); - isOwn = authorId == GameMain.Client.ID; + isOwn = authorId == GameMain.Client.SessionId; } ushort clientCount = inc.ReadUInt16(); @@ -196,7 +196,7 @@ namespace Barotrauma } break; case ReadyCheckState.Update: - ReadyStatus newState = (ReadyStatus) inc.ReadByte(); + ReadyStatus newState = (ReadyStatus)inc.ReadByte(); byte targetId = inc.ReadByte(); if (crewManager.ActiveReadyCheck != null) { @@ -208,7 +208,7 @@ namespace Barotrauma for (int i = 0; i < count; i++) { byte id = inc.ReadByte(); - ReadyStatus status = (ReadyStatus) inc.ReadByte(); + ReadyStatus status = (ReadyStatus)inc.ReadByte(); crewManager.ActiveReadyCheck?.UpdateState(id, status); } @@ -269,9 +269,9 @@ namespace Barotrauma private static void SendState(ReadyStatus status) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ClientPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.Update); - msg.Write((byte) status); + msg.WriteByte((byte)ClientPacketHeader.READY_CHECK); + msg.WriteByte((byte)ReadyCheckState.Update); + msg.WriteByte((byte)status); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } @@ -283,8 +283,8 @@ namespace Barotrauma ReadyCheckCooldown = DateTime.Now.AddMinutes(1); #endif IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ClientPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.Start); + msg.WriteByte((byte)ClientPacketHeader.READY_CHECK); + msg.WriteByte((byte)ReadyCheckState.Start); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 490516616..1645e1be1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -562,7 +562,7 @@ namespace Barotrauma break; } //if putting an item to a container with a max stack size of 1, only put one item from the stack - if (quickUseAction == QuickUseAction.PutToContainer && (character.SelectedConstruction?.GetComponent()?.MaxStackSize ?? 0) <= 1) + if (quickUseAction == QuickUseAction.PutToContainer && (character.SelectedItem?.GetComponent()?.MaxStackSize ?? 0) <= 1) { break; } @@ -595,14 +595,14 @@ namespace Barotrauma if (rootInventory != null && rootInventory.Owner != Character.Controlled && - rootInventory.Owner != Character.Controlled.SelectedConstruction && + rootInventory.Owner != Character.Controlled.SelectedItem && rootInventory.Owner != Character.Controlled.SelectedCharacter) { //allow interacting if the container is linked to the item the character is interacting with if (!(rootContainer != null && rootContainer.DisplaySideBySideWhenLinked && - Character.Controlled.SelectedConstruction != null && - rootContainer.linkedTo.Contains(Character.Controlled.SelectedConstruction))) + Character.Controlled.SelectedItem != null && + rootContainer.linkedTo.Contains(Character.Controlled.SelectedItem))) { DraggingItems.Clear(); } @@ -756,7 +756,7 @@ namespace Barotrauma } else { - var selectedContainer = character.SelectedConstruction?.GetComponent(); + var selectedContainer = character.SelectedItem?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && !selectedContainer.Inventory.Locked) @@ -775,7 +775,8 @@ namespace Barotrauma else { bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); - var selectedContainer = character.SelectedConstruction?.GetComponent(); + var selectedContainer = character.SelectedItem?.GetComponent(); + if (selectedContainer != null && selectedContainer.Inventory != null && !selectedContainer.Inventory.Locked && @@ -930,7 +931,7 @@ namespace Barotrauma } break; case QuickUseAction.PutToContainer: - var selectedContainer = character.SelectedConstruction?.GetComponent(); + var selectedContainer = character.SelectedItem?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null) { //player has selected the inventory of another item -> attempt to move the item there @@ -965,8 +966,8 @@ namespace Barotrauma } break; case QuickUseAction.PutToEquippedItem: - - foreach (Item heldItem in character.HeldItems) + //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank + foreach (Item heldItem in character.HeldItems.OrderBy(it => it.ContainedItems.FirstOrDefault()?.Condition ?? 0.0f)) { if (heldItem.OwnInventory == null) { continue; } //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 88bd89496..aa7baf8be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma.Items.Components { @@ -177,7 +178,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - Color color = item.SpriteColor; + Color color = item.GetSpriteColor(withHighlight: true); if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured @@ -220,11 +221,15 @@ namespace Barotrauma.Items.Components color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); } - if (brokenSprite != null && item.Health < item.MaxCondition) + float maxCondition = item.Repairables.Any() ? + item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition : + item.MaxCondition; + float healthRatio = item.Health / maxCondition; + if (brokenSprite != null && healthRatio < 1.0f) { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition) : Vector2.One; + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One; if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; + float alpha = fadeBrokenSprite ? 1.0f - healthRatio : 1.0f; spriteBatch.Draw(brokenSprite.Texture, pos, getSourceRect(brokenSprite, openState, IsHorizontal), color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, item.SpriteEffects, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index 8428d53ff..5b8af4a6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -166,10 +166,10 @@ namespace Barotrauma.Items.Components List tiles = new List(); for (int i = 0; i < vineCount; i++) { - VineTileType vineType = (VineTileType) msg.ReadRangedInteger(0b0000, 0b1111); + VineTileType vineType = (VineTileType)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); int leafConfig = msg.ReadRangedInteger(0, 0xFFF); - sbyte posX = (sbyte) msg.ReadByte(), posY = (sbyte) msg.ReadByte(); + sbyte posX = (sbyte)msg.ReadByte(), posY = (sbyte)msg.ReadByte(); Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); tiles.Add(new VineTile(this, pos, vineType, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 77da549e2..5380f8940 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -69,8 +69,8 @@ namespace Barotrauma.Items.Components var eventData = ExtractEventData(extraData); Vector2 attachPos = eventData.AttachPos; - msg.Write(attachPos.X); - msg.Write(attachPos.Y); + msg.WriteSingle(attachPos.X); + msg.WriteSingle(attachPos.Y); } public override void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index bd2b6da47..e19b1dd82 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -23,11 +23,10 @@ namespace Barotrauma.Items.Components public void ExtractJobPrefab(IReadOnlyDictionary tags) { - if (!tags.TryGetValue("jobid".ToIdentifier(), out string jobId)) { return; } - + if (!tags.TryGetValue("jobid".ToIdentifier(), out string jobId)) { return; } if (!jobId.IsNullOrEmpty()) { - JobPrefab = JobPrefab.Get(jobId); + JobPrefab = JobPrefab.Get(jobId.ToIdentifier()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 39982b6cd..8a605499d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; if (GUI.HideCursor) { - crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale); + crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index d331fc8a7..345411caa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -66,6 +66,8 @@ namespace Barotrauma.Items.Components public float IsActiveTimer; + public virtual bool RecreateGUIOnResolutionChange => false; + public GUILayoutSettings DefaultLayout { get; protected set; } public GUILayoutSettings AlternativeLayout { get; protected set; } @@ -148,6 +150,8 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } + public bool LockGuiFramePosition; + [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap { @@ -365,6 +369,7 @@ namespace Barotrauma.Items.Components loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(position.X, position.Y, 0.0f), 0.01f, + freqMult: itemSound.RoundSound.GetRandomFrequencyMultiplier(), muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; //TODO: tweak @@ -522,13 +527,11 @@ namespace Barotrauma.Items.Components if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); if (!soundSelectionModes.ContainsKey(type) || soundSelectionModes[type] == SoundSelectionMode.Random) { - SoundSelectionMode selectionMode = SoundSelectionMode.Random; - Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out selectionMode); + Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode); soundSelectionModes[type] = selectionMode; } - List soundList = null; - if (!sounds.TryGetValue(itemSound.Type, out soundList)) + if (!sounds.TryGetValue(itemSound.Type, out List soundList)) { soundList = new List(); sounds.Add(itemSound.Type, soundList); @@ -566,12 +569,86 @@ namespace Barotrauma.Items.Components } string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas, Anchor.Center), style, color); + + TryCreateDragHandle(); + DefaultLayout = GUILayoutSettings.Load(GuiFrameSource); if (GuiFrame != null) { GuiFrame.RectTransform.ParentChanged += OnGUIParentChanged; } - GameMain.Instance.ResolutionChanged += OnResolutionChanged; + GameMain.Instance.ResolutionChanged += OnResolutionChangedPrivate; + } + + protected virtual void TryCreateDragHandle() + { + if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) + { + var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), + GuiFrame.RectTransform, style: null) + { + DragArea = HUDLayoutSettings.ItemHUDArea + }; + + int iconHeight = GUIStyle.ItemFrameMargin.Y / 4; + var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), handle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, + style: "GUIDragIndicatorHorizontal"); + dragIcon.RectTransform.MinSize = new Point(0, iconHeight); + + handle.ValidatePosition = (RectTransform rectT) => + { + var activeHuds = Character.Controlled?.SelectedItem?.ActiveHUDs ?? item.ActiveHUDs; + foreach (ItemComponent ic in activeHuds) + { + if (ic == this || ic.GuiFrame == null || !ic.CanBeSelected) { continue; } + if (ic.GuiFrame.Rect.Width > GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height > GameMain.GraphicsHeight * 0.9f) + { + //a full-screen GUIFrame (or at least close to one) - this component is doing something weird, + //an ItemContainer with no GUIFrame definition that positions itself in some other GUIFrame, some kind of an overlay? + // -> allow intersecting + continue; + } + if (dragIcon.Rect.Intersects(ic.GuiFrame.Rect)) + { + GuiFrame.ImmediateFlash(); + return false; + } + } + foreach (ItemComponent ic in activeHuds) + { + //refresh slots to ensure they're rendered at the correct position + (ic as ItemContainer)?.Inventory.CreateSlots(); + } + return true; + }; + + int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); + new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + style: "GUIButtonSettings") + { + OnClicked = (btn, userdata) => + { + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("item.resetuiposition", isEnabled: true, onSelected: () => + { + if (Character.Controlled?.SelectedItem != null && item != Character.Controlled.SelectedItem) + { + Character.Controlled.SelectedItem.ForceHUDLayoutUpdate(ignoreLocking: true); + } + else + { + item.ForceHUDLayoutUpdate(ignoreLocking: true); + } + }), + new ContextMenuOption(TextManager.Get(LockGuiFramePosition ? "item.unlockuiposition" : "item.lockuiposition"), isEnabled: true, onSelected: () => + { + LockGuiFramePosition = !LockGuiFramePosition; + handle.Enabled = !LockGuiFramePosition; + })); + return true; + } + }; + } } /// @@ -631,6 +708,11 @@ namespace Barotrauma.Items.Components CreateGUI(); } OnResolutionChanged(); + item.ForceHUDLayoutUpdate(ignoreLocking: true); + if (GuiFrame != null && GuiFrame.GetChild() is GUIDragHandle dragHandle) + { + dragHandle.DragArea = HUDLayoutSettings.ItemHUDArea; + } } public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index b1d894203..d43d947fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -45,6 +45,8 @@ namespace Barotrauma.Items.Components private set; } + public override bool RecreateGUIOnResolutionChange => true; + /// /// Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used. /// @@ -189,7 +191,7 @@ namespace Barotrauma.Items.Components onDraw: (SpriteBatch spriteBatch, GUICustomComponent component) => { Inventory.Draw(spriteBatch); }, onUpdate: null) { - CanBeFocused = false + CanBeFocused = true }; // Expand the frame vertically if it's too small to fit the text @@ -322,6 +324,8 @@ namespace Barotrauma.Items.Components int i = 0; foreach (Item containedItem in Inventory.AllItems) { + if (containedItem?.Sprite == null) { continue; } + if (AutoInteractWithContained) { containedItem.IsHighlighted = item.IsHighlighted; @@ -342,7 +346,7 @@ namespace Barotrauma.Items.Components containedItem.Sprite.Draw( spriteBatch, new Vector2(currentItemPos.X, -currentItemPos.Y), - isWiringMode ? containedItem.GetSpriteColor() * 0.15f : containedItem.GetSpriteColor(), + isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), origin, -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation ), containedItem.Scale, @@ -381,10 +385,15 @@ namespace Barotrauma.Items.Components guiCustomComponent.RectTransform.Parent = Inventory.RectTransform; } + if (item.ParentInventory?.Owner == character && character.SelectedItem == item) + { + character.SelectedItem = null; + } + //if the item is in the character's inventory, no need to update the item's inventory - //because the player can see it by hovering the cursor over the item - guiCustomComponent.Visible = item.ParentInventory?.Owner != character && DrawInventory; - if (!guiCustomComponent.Visible) { return; } + //because the player can see it by hovering the cursor over the item + guiCustomComponent.Visible = DrawInventory && item.ParentInventory?.Owner != character; + if (!guiCustomComponent.Visible) { return; } Inventory.Update(deltaTime, cam); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 5bfc4e192..332f924cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -90,8 +90,8 @@ namespace Barotrauma.Items.Components { Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; - SetLightSourceTransformProjSpecific(); } + SetLightSourceTransformProjSpecific(); } partial void OnStateChanged() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 52c211c18..dd3b30b66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -1,9 +1,11 @@ -using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -15,6 +17,7 @@ namespace Barotrauma.Items.Components } private GUIButton activateButton; private GUIComponent inputInventoryHolder, outputInventoryHolder; + private GUIListBox outputDisplayListBox; private GUIComponent inSufficientPowerWarning; @@ -31,45 +34,60 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, IsPropertySaveable.Yes)] public float InfoAreaWidth { get; set; } - partial void InitProjSpecific(XElement element) + [Serialize(true, IsPropertySaveable.Yes)] + public bool ShowOutput { get; set; } + + partial void InitProjSpecific(XElement _) { CreateGUI(); } + public override bool RecreateGUIOnResolutionChange => true; + protected override void OnResolutionChanged() { - base.OnResolutionChanged(); OnItemLoadedProjSpecific(); } protected override void CreateGUI() { - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.90f, 0.80f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { - Stretch = true, + Stretch = true, RelativeSpacing = 0.08f }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform) { MinSize = new Point(0, GUI.IntScale(25)) }, item.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleHorizontal = true }; - var topFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedFrame.RectTransform), style: null); - + var topFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.375f), paddedFrame.RectTransform), style: null); + // === INPUT LABEL === // - var inputLabelArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), topFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; - inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); - new GUIFrame(new RectTransform(Vector2.One, inputLabelArea.RectTransform), style: "HorizontalLine"); + var inputLabelArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), topFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true); + + var queueLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.43f, 1f), inputLabelArea.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + var queueLabel = new GUITextBlock(new RectTransform(Vector2.One, queueLabelLayout.RectTransform), TextManager.Get("deconstructor.inputqueue"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + queueLabel.RectTransform.Resize(new Point((int) queueLabel.Font.MeasureString(queueLabel.Text).X, queueLabel.RectTransform.Rect.Height)); + new GUIFrame(new RectTransform(Vector2.One, queueLabelLayout.RectTransform), style: "HorizontalLine"); + + var inputLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.57f, 1f), inputLabelArea.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelLayout.RectTransform), TextManager.Get("deconstructor.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); + new GUIFrame(new RectTransform(Vector2.One, inputLabelLayout.RectTransform), style: "HorizontalLine"); var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), topFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; - + // === INPUT SLOTS === // inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawOverLay, null) { CanBeFocused = false }; @@ -78,6 +96,7 @@ namespace Barotrauma.Items.Components var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterLeft); activateButton = new GUIButton(new RectTransform(new Vector2(0.95f, 0.8f), buttonContainer.RectTransform), TextManager.Get("DeconstructorDeconstruct"), style: "DeviceButton") { + UserData = UIHighlightAction.ElementId.DeconstructButton, TextBlock = { AutoScaleHorizontal = true }, OnClicked = OnActivateButtonClicked }; @@ -92,8 +111,8 @@ namespace Barotrauma.Items.Components }; // === OUTPUT AREA === // - var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedFrame.RectTransform), style: null); - + var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.375f), paddedFrame.RectTransform), style: null); + // === OUTPUT LABEL === // var outputLabelArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), bottomFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true) { @@ -104,10 +123,19 @@ namespace Barotrauma.Items.Components outputLabel.RectTransform.Resize(new Point((int) outputLabel.Font.MeasureString(outputLabel.Text).X, outputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, outputLabelArea.RectTransform), style: "HorizontalLine"); - var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; + var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; - // === OUTPUT SLOTS === // - outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null); + // === OUTPUT SLOTS === // + outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null); + + if (ShowOutput) + { + GUILayoutGroup outputDisplayLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedFrame.RectTransform), childAnchor: Anchor.TopCenter); + GUILayoutGroup outDisplayTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), outputDisplayLayout.RectTransform), isHorizontal: true); + GUITextBlock outDisplayBlock = new GUITextBlock(new RectTransform(Vector2.One, outDisplayTopGroup.RectTransform), TextManager.Get("deconstructor.output"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + GUILayoutGroup outDisplayBottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.975f, 0.8f), outputDisplayLayout.RectTransform), isHorizontal: true); + outputDisplayListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f), outDisplayBottomGroup.RectTransform), isHorizontal: true, style: null); + } if (InfoAreaWidth >= 0.0f) { @@ -181,7 +209,7 @@ namespace Barotrauma.Items.Components { if (!(linkedTo is Item { DisplaySideBySideWhenLinked: true } linkedItem)) { continue; } if (!linkedItem.Components.Any()) { continue; } - + var itemContainer = linkedItem.GetComponent(); if (itemContainer?.GuiFrame == null || itemContainer.AllowUIOverlap) { continue; } @@ -195,12 +223,178 @@ namespace Barotrauma.Items.Components return base.Select(character); } + partial void OnItemSlotsChanged(ItemContainer container) + { + if (container.Inventory is null) { return; } + RefreshOutputDisplay(container.Inventory.AllItems.ToImmutableArray()); + } + + private void RefreshOutputDisplay(ImmutableArray items) + { + const string outputItemCountUserData = "OutputItemCount"; + const string questionMarkUserData = "UnknownItemOutput"; + + if (outputDisplayListBox is null || inputContainer.Inventory is null) { return; } + + Dictionary itemCounts = new Dictionary(); + Dictionary children = new Dictionary(); + + bool addQuestionMark = false; + + foreach (GUIComponent child in outputDisplayListBox.Content.Children) + { + if (child.UserData is Identifier it) + { + children.Add(it, child); + } + } + + if (outputDisplayListBox.Content.FindChild(questionMarkUserData) is { } foundChild) + { + outputDisplayListBox.RemoveChild(foundChild); + } + + foreach (Item it in items) + { + if (it.Prefab.RandomDeconstructionOutput) + { + addQuestionMark = true; + continue; + } + + foreach (DeconstructItem deconstructItem in it.Prefab.DeconstructItems) + { + if (!deconstructItem.IsValidDeconstructor(item)) { continue; } + RegisterItem(deconstructItem.ItemIdentifier, deconstructItem.Amount); + } + + /*if (it.OwnInventory is { } inventory) + { + foreach (Item inventoryItems in inventory.AllItems) + { + RegisterItem(inventoryItems.Prefab.Identifier); + } + }*/ + + void RegisterItem(Identifier identifier, int amount = 1) + { + if (itemCounts.ContainsKey(identifier)) + { + itemCounts[identifier] += amount; + return; + } + itemCounts.Add(identifier, amount); + } + } + + foreach (var (it, child) in children) + { + if (!itemCounts.ContainsKey(it)) + { + outputDisplayListBox.RemoveChild(child); + } + } + + foreach (var (it, amount) in itemCounts) + { + if (!children.TryGetValue(it, out GUIComponent child)) + { + child = CreateOutputDisplayItem(it, outputDisplayListBox.Content); + } + + if (child is null) { continue; } + UpdateOutputDisplayItemCount(child, amount); + } + + if (addQuestionMark) + { + CreateQuestionMark(outputDisplayListBox.Content); + } + + static void CreateQuestionMark(GUIComponent parent) + { + GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), parent.RectTransform), style: null) + { + UserData = questionMarkUserData, + ToolTip = TextManager.Get("deconstructor.unknownitemsoutput") + }; + + GUIFrame questionMarkFrame = new GUIFrame(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), style: "GUIFrameListBox") + { + CanBeFocused = false, + }; + + // question mark text + new GUITextBlock(new RectTransform(Vector2.One, questionMarkFrame.RectTransform, anchor: Anchor.Center), text: "?", textAlignment: Alignment.Center, font: GUIStyle.LargeFont) + { + CanBeFocused = false + }; + } + + static GUIComponent CreateOutputDisplayItem(Identifier identifier, GUIComponent parent) + { + ItemPrefab prefab = ItemPrefab.Find(null, identifier); + if (prefab is null) { return null; } + + GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), parent.RectTransform), style: null) + { + UserData = identifier, + ToolTip = GetTooltip(prefab) + }; + + Sprite icon = prefab.InventoryIcon ?? prefab.Sprite; + Color iconColor = prefab.InventoryIcon is null ? prefab.SpriteColor : prefab.InventoryIconColor; + + GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true) + { + Color = iconColor, + CanBeFocused = false + }; + + // item count text + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), itemIcon.RectTransform, anchor: Anchor.BottomRight), "", font: GUIStyle.Font, textAlignment: Alignment.BottomRight) + { + UserData = outputItemCountUserData, + Shadow = true, + CanBeFocused = false, + Padding = Vector4.Zero, + TextColor = Color.White, + }; + + return itemFrame; + } + + static void UpdateOutputDisplayItemCount(GUIComponent component, int count) + { + if (!(component.FindChild(outputItemCountUserData, recursive: true) is GUITextBlock textBlock)) { return; } + + textBlock.Text = TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString()); + } + + static RichString GetTooltip(ItemPrefab prefab) + { + LocalizedString toolTip = $"‖color:{Color.White.ToStringHex()}‖{prefab.Name}‖color:end‖"; + + LocalizedString description = prefab.Description; + if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } + + if (prefab.ContentPackage != GameMain.VanillaContent && prefab.ContentPackage != null) + { + toolTip += $"\n‖color:{Color.MediumPurple.ToStringHex()}‖{prefab.ContentPackage.Name}‖color:end‖"; + } + + return RichString.Rich(toolTip); + } + } + partial void OnItemLoadedProjSpecific() { inputContainer.AllowUIOverlap = true; inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; outputContainer.AllowUIOverlap = true; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; + + inputContainer.Inventory.Locked = IsActive; } private void DrawOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) @@ -208,7 +402,7 @@ namespace Barotrauma.Items.Components overlayComponent.RectTransform.SetAsLastChild(); if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } - + if (DeconstructItemsSimultaneously) { for (int i = 0; i < InputContainer.Inventory.Capacity; i++) @@ -239,17 +433,22 @@ namespace Barotrauma.Items.Components private bool OnActivateButtonClicked(GUIButton button, object obj) { - var disallowedItem = inputContainer.Inventory.FindItem(i => !i.AllowDeconstruct, recursive: false); - if (disallowedItem != null && !DeconstructItemsSimultaneously) + if (!IsActive) { - int index = inputContainer.Inventory.FindIndex(disallowedItem); - if (index >= 0 && index < inputContainer.Inventory.visualSlots.Length) + //don't allow turning on if there's non-deconstructable items in the queue + var disallowedItem = inputContainer.Inventory.FindItem(i => !i.AllowDeconstruct, recursive: false); + if (disallowedItem != null && !DeconstructItemsSimultaneously) { - var slot = inputContainer.Inventory.visualSlots[index]; - slot?.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); + int index = inputContainer.Inventory.FindIndex(disallowedItem); + if (index >= 0 && index < inputContainer.Inventory.visualSlots.Length) + { + var slot = inputContainer.Inventory.visualSlots[index]; + slot?.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); + } + return true; } - return true; } + if (GameMain.Client != null) { pendingState = !IsActive; @@ -264,7 +463,7 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write(pendingState); + msg.WriteBoolean(pendingState); } public void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 90849cd19..16e766617 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -32,6 +32,8 @@ namespace Barotrauma.Items.Components } private FabricationRecipe selectedItem; + public Identifier SelectedItemIdentifier => SelectedItem?.TargetItem.Identifier ?? Identifier.Empty; + private GUIComponent inSufficientPowerWarning; private FabricationRecipe pendingFabricatedItem; @@ -51,11 +53,13 @@ namespace Barotrauma.Items.Components [Serialize("vendingmachine.outofstock", IsPropertySaveable.Yes)] public string FabricationLimitReachedText { get; set; } + public override bool RecreateGUIOnResolutionChange => true; + protected override void OnResolutionChanged() { if (GuiFrame != null) { - OnItemLoadedProjSpecific(); + InitInventoryUIs(); } } @@ -232,6 +236,17 @@ namespace Barotrauma.Items.Components } } + private void InitInventoryUIs() + { + if (inputInventoryHolder != null) + { + inputContainer.AllowUIOverlap = true; + inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; + } + outputContainer.AllowUIOverlap = true; + outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; + } + private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) { if (fabricationRecipe == null) { return ""; } @@ -249,13 +264,7 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific() { CreateGUI(); - if (inputInventoryHolder != null) - { - inputContainer.AllowUIOverlap = true; - inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; - } - outputContainer.AllowUIOverlap = true; - outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; + InitInventoryUIs(); } partial void SelectProjSpecific(Character character) @@ -328,76 +337,112 @@ namespace Barotrauma.Items.Components } } + private readonly Dictionary missingIngredientCounts = new Dictionary(); + private float ingredientHighlightTimer; + private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); + missingIngredientCounts.Clear(); + FabricationRecipe targetItem = fabricatedItem ?? selectedItem; if (targetItem != null) { - int slotIndex = 0; - - var missingItems = new List(); - foreach (FabricationRecipe.RequiredItem requiredItem in targetItem.RequiredItems) { - for (int i = 0; i < requiredItem.Amount; i++) + if (missingIngredientCounts.ContainsKey(requiredItem)) { - missingItems.Add(requiredItem); + missingIngredientCounts[requiredItem] += requiredItem.Amount; + } + else + { + missingIngredientCounts[requiredItem] = requiredItem.Amount; } } foreach (Item item in inputContainer.Inventory.AllItems) { - missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.Prefab))); - } - var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); - missingItems = missingItems.Distinct().ToList(); + var missingIngredient = missingIngredientCounts.Keys.FirstOrDefault(mi => mi.MatchesItem(item)); + if (missingIngredient == null) { continue; } - foreach (FabricationRecipe.RequiredItem requiredItem in missingItems) + if (missingIngredientCounts[missingIngredient] == 1) + { + missingIngredientCounts.Remove(missingIngredient); + } + else + { + missingIngredientCounts[missingIngredient]--; + } + } + + if (ingredientHighlightTimer <= 0.0f) { + //highlight inventory slots that contain suitable ingredients in linked inventories + foreach (var inventory in linkedInventories) + { + if (inventory.visualSlots == null) { continue; } + for (int i = 0; i < inventory.Capacity; i++) + { + if (inventory.visualSlots[i].HighlightTimer > 0.0f) { continue; } + var availableItem = inventory.GetItemAt(i); + if (availableItem == null) { continue; } + + if (missingIngredientCounts.Keys.Any(it => it.MatchesItem(availableItem))) + { + inventory.visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); + continue; + } + if (availableItem.OwnInventory != null) + { + for (int j = 0; j < availableItem.OwnInventory.Capacity; j++) + { + var availableContainedItem = availableItem.OwnInventory.GetItemAt(i); + if (availableContainedItem == null) { continue; } + if (missingIngredientCounts.Keys.Any(it => it.MatchesItem(availableContainedItem))) + { + inventory.visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); + break; + } + } + } + } + } + ingredientHighlightTimer = 1.0f; + } + + int slotIndex = 0; + foreach (var kvp in missingIngredientCounts) + { + var requiredItem = kvp.Key; + int missingCount = kvp.Value; + while (slotIndex < inputContainer.Capacity && inputContainer.Inventory.GetItemAt(slotIndex) != null) { slotIndex++; } - requiredItem.ItemPrefabs - .Where(requiredPrefab => availableIngredients.ContainsKey(requiredPrefab.Identifier)) - .ForEach(requiredPrefab => { - var availableItems = availableIngredients[requiredPrefab.Identifier]; - foreach (Item it in availableItems) - { - if (it.ParentInventory == inputContainer.Inventory) { continue; } - var rootInventoryOwner = it.GetRootInventoryOwner(); - Inventory rootInventory = (rootInventoryOwner as Item)?.OwnInventory as Inventory ?? (rootInventoryOwner as Character)?.Inventory; - if (rootInventory?.visualSlots == null) { continue; } - int availableSlotIndex = rootInventory.FindIndex((it.Container != rootInventoryOwner ? it.Container : it) ?? it); - if (availableSlotIndex < 0) { continue; } - if (rootInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) - { - rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); - if (slotIndex < inputContainer.Capacity) - { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); - } - } - } - }); - if (slotIndex >= inputContainer.Capacity) { break; } - var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().Sprite; + if (slotIndex < inputContainer.Capacity && + inputContainer.Inventory.visualSlots[slotIndex].HighlightTimer <= 0.0f && + availableIngredients.Any(i => i.Value.Any() && requiredItem.MatchesItem(i.Value.First()))) + { + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); + } + + var requiredItemPrefab = requiredItem.FirstMatchingPrefab; + var itemIcon = requiredItemPrefab.InventoryIcon ?? requiredItemPrefab.Sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), - color: requiredItem.ItemPrefabs.First().InventoryIconColor * 0.3f, + color: requiredItemPrefab.InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); - - - if (missingCounts[requiredItem] > 1) + + if (missingCount > 1) { Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); - string stackCountText = "x" + missingCounts[requiredItem]; + string stackCountText = "x" + missingCount; stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); @@ -446,9 +491,9 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } - if (!requiredItem.ItemPrefabs.First().Description.IsNullOrEmpty()) + if (!requiredItemPrefab.Description.IsNullOrEmpty()) { - toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; + toolTipText += '\n' + requiredItemPrefab.Description; } tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } @@ -715,6 +760,8 @@ namespace Barotrauma.Items.Components activateButton.Enabled = false; inSufficientPowerWarning.Visible = IsActive && !hasPower; + ingredientHighlightTimer -= deltaTime; + if (!IsActive) { //only check ingredients if the fabricator isn't active (if it is, this is done in Update) @@ -763,7 +810,7 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; - msg.Write(recipeHash); + msg.WriteUInt32(recipeHash); } public void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 371d3882b..3b25c87b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -246,6 +246,7 @@ namespace Barotrauma.Items.Components protected override void CreateGUI() { GuiFrame.ClearChildren(); + TryCreateDragHandle(); GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); GuiFrame.CanBeFocused = true; @@ -509,7 +510,7 @@ namespace Barotrauma.Items.Components Vector2 origin = weaponSprite.Origin; float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; - weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); + weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None); } }); @@ -1012,12 +1013,14 @@ namespace Barotrauma.Items.Components } else if (hullData.LinkedHulls.Any()) { - hullData.HullWaterAmount = 0.0f; + float waterVolume = 0.0f; + float totalVolume = 0.0f; foreach (Hull linkedHull in hullData.LinkedHulls) { - hullData.HullWaterAmount += WaterDetector.GetWaterPercentage(linkedHull); + waterVolume += linkedHull.WaterVolume; + totalVolume += linkedHull.Volume; } - hullData.HullWaterAmount /= hullData.LinkedHulls.Count; + hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 2798830ae..8c09ef322 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (Character.Controlled?.SelectedConstruction != item) + if (Character.Controlled?.SelectedItem != item) { IsActive = false; return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index f3bfe988b..d3076f105 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -1,10 +1,8 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -58,6 +56,7 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(0, 0.1f) }, style: "PowerButton") { + UserData = UIHighlightAction.ElementId.PowerButton, OnClicked = (button, data) => { TargetLevel = null; @@ -114,6 +113,7 @@ namespace Barotrauma.Items.Components return true; } }; + pumpSpeedSlider.Frame.UserData = UIHighlightAction.ElementId.PumpSpeedSlider; var textsArea = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), sliderArea.RectTransform, Anchor.BottomCenter), style: null); var outLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textsArea.RectTransform, Anchor.CenterLeft), TextManager.Get("PumpOut"), textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterLeft, wrap: false, font: GUIStyle.SubHeadingFont); @@ -219,7 +219,7 @@ namespace Barotrauma.Items.Components { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); - msg.Write(IsActive); + msg.WriteBoolean(IsActive); } public void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index cfe675dbb..b229b2142 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -1,11 +1,11 @@ using Barotrauma.Extensions; using Barotrauma.Networking; +using Barotrauma.Sounds; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -34,12 +34,14 @@ namespace Barotrauma.Items.Components private Color optimalRangeColor = new Color(74,238,104,255); private Color offRangeColor = Color.Orange; private Color warningColor = Color.Red; - private Color coldColor = Color.LightBlue; - private Color warmColor = Color.Orange; - private Color hotColor = Color.Red; + + private readonly Color[] temperatureColors = new Color[] { Color.Blue, Color.LightBlue, Color.Orange, Color.Red }; private Color outputColor = Color.Goldenrod; private Color loadColor = Color.LightSteelBlue; + private RoundSound temperatureBoostSoundUp, temperatureBoostSoundDown; + private GUIButton temperatureBoostUpButton, temperatureBoostDownButton; + public GUIScrollBar FissionRateScrollBar { get; private set; } public GUIScrollBar TurbineOutputScrollBar { get; private set; } @@ -63,10 +65,11 @@ namespace Barotrauma.Items.Components "ReactorWarningOverheating", "ReactorWarningHighOutput", "ReactorWarningFuelOut", "ReactorWarningSCRAM" }; + public override bool RecreateGUIOnResolutionChange => true; + partial void InitProjSpecific(ContentXElement element) { - // TODO: need to recreate the gui when the resolution changes - + CreateGUI(); fissionRateMeter = new Sprite(element.GetChildElement("fissionratemeter")?.GetChildElement("sprite")); turbineOutputMeter = new Sprite(element.GetChildElement("turbineoutputmeter")?.GetChildElement("sprite")); meterPointer = new Sprite(element.GetChildElement("meterpointer")?.GetChildElement("sprite")); @@ -76,10 +79,28 @@ namespace Barotrauma.Items.Components tempRangeIndicator = new Sprite(element.GetChildElement("temprangeindicator")?.GetChildElement("sprite")); graphLine = new Sprite(element.GetChildElement("graphline")?.GetChildElement("sprite")); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "temperatureboostsoundup": + temperatureBoostSoundUp = RoundSound.Load(subElement, false); + break; + case "temperatureboostsounddown": + temperatureBoostSoundDown = RoundSound.Load(subElement, false); + break; + } + } + } + + protected override void CreateGUI() + { + warningButtons.Clear(); + paddedFrame = new GUILayoutGroup(new RectTransform( - GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) - { AbsoluteOffset = GUIStyle.ItemFrameOffset }, - isHorizontal: true) + GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) + { AbsoluteOffset = GUIStyle.ItemFrameOffset }, + isHorizontal: true) { RelativeSpacing = 0.012f, Stretch = true @@ -213,6 +234,7 @@ namespace Barotrauma.Items.Components return false; } }; + FissionRateScrollBar.Frame.UserData = UIHighlightAction.ElementId.FissionRateSlider; TurbineOutputScrollBar = new GUIScrollBar(new RectTransform(sliderSize, rightArea.RectTransform, Anchor.TopCenter) { @@ -231,6 +253,7 @@ namespace Barotrauma.Items.Components return false; } }; + TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider; var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform)) { @@ -281,9 +304,10 @@ namespace Barotrauma.Items.Components new GUIFrame(new RectTransform(new Vector2(0.01f, 1.0f), topRightArea.RectTransform), style: "VerticalLine"); - AutoTempSwitch = new GUIButton(new RectTransform(new Vector2(0.15f, 0.9f), topRightArea.RectTransform), + AutoTempSwitch = new GUIButton(new RectTransform(new Vector2(0.15f, 0.9f), topRightArea.RectTransform), style: "SwitchVertical") { + UserData = UIHighlightAction.ElementId.AutoTempSwitch, Enabled = false, Selected = AutoTemp, ClickSound = GUISoundType.UISwitch, @@ -326,6 +350,7 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(0, 0.1f) }, style: "PowerButton") { + UserData = UIHighlightAction.ElementId.PowerButton, OnClicked = (button, data) => { PowerOn = !PowerOn; @@ -354,7 +379,46 @@ namespace Barotrauma.Items.Components new GUIFrame(new RectTransform(new Vector2(0.01f, 1.0f), bottomRightArea.RectTransform), style: "VerticalLine"); - new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1), bottomRightArea.RectTransform, Anchor.Center), DrawTempMeter, null); + var temperatureArea = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 1), bottomRightArea.RectTransform, Anchor.Center), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + temperatureBoostUpButton = new GUIButton(new RectTransform(Vector2.One, temperatureArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: "GUIPlusButton") + { + ToolTip = TextManager.Get("reactor.temperatureboostup"), + OnClicked = (_, __) => + { + applyTemperatureBoost(TemperatureBoostAmount, temperatureBoostSoundUp); + return true; + } + }; + new GUICustomComponent(new RectTransform(Vector2.One, temperatureArea.RectTransform, Anchor.Center), DrawTempMeter, null); + + temperatureBoostDownButton = new GUIButton(new RectTransform(Vector2.One, temperatureArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: "GUIMinusButton") + { + ToolTip = TextManager.Get("reactor.temperatureboostdown"), + OnClicked = (_, __) => + { + applyTemperatureBoost(-TemperatureBoostAmount, temperatureBoostSoundDown); + return true; + } + }; + + void applyTemperatureBoost(float amount, RoundSound sound) + { + temperatureBoost = amount; + if (sound != null) + { + SoundPlayer.PlaySound( + sound.Sound, + item.WorldPosition, + sound.Volume, + sound.Range, + hullGuess: item.CurrentHull); + } + } var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), bottomRightArea.RectTransform)) { @@ -382,6 +446,19 @@ namespace Barotrauma.Items.Components }; LocalizedString outputStr = TextManager.Get("ReactorOutput"); outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}"; + + InitInventoryUI(); + } + + private void InitInventoryUI() + { + var itemContainer = item.GetComponent(); + if (itemContainer != null) + { + itemContainer.UILabel = ""; + itemContainer.AllowUIOverlap = true; + itemContainer.Inventory.RectTransform = inventoryContainer.RectTransform; + } } public override void OnItemLoaded() @@ -389,23 +466,10 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; - var itemContainer = item.GetComponent(); - if (itemContainer != null) - { - itemContainer.UILabel = ""; - itemContainer.AllowUIOverlap = true; - itemContainer.Inventory.RectTransform = inventoryContainer.RectTransform; - /*var inventoryLabel = inventoryContainer.Parent?.GetChild(); - if (inventoryLabel != null) - { - inventoryLabel.RectTransform.MinSize = new Point(100, 0); - inventoryLabel.Text = itemContainer.GetUILabel(); - inventoryLabel.CalculateHeightFromText(); - (inventoryLabel.Parent as GUILayoutGroup).Recalculate(); - }*/ - } + InitInventoryUI(); } + private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container) { Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y); @@ -418,7 +482,7 @@ namespace Barotrauma.Items.Components while (meterBarPos.Y > container.Rect.Bottom + (int)(5 * GUI.yScale) - container.Rect.Height * tempFill) { float tempRatio = 1.0f - ((meterBarPos.Y - container.Rect.Y) / container.Rect.Height); - Color color = ToolBox.GradientLerp(tempRatio, coldColor, optimalRangeColor, warmColor, hotColor); + Color color = ToolBox.GradientLerp(tempRatio, temperatureColors); tempMeterBar.Draw(spriteBatch, meterBarPos, color: color, scale: meterBarScale); int spacing = 2; meterBarPos.Y -= tempMeterBar.size.Y * meterBarScale + spacing; @@ -635,7 +699,6 @@ namespace Barotrauma.Items.Components float normalizedValue = (value - range.X) / (range.Y - range.X); float valueRad = MathHelper.Lerp(sectorRad.X, sectorRad.Y, normalizedValue); - Vector2 offset = new Vector2(0, 40) * scale; meterPointer.Draw(spriteBatch, pointerPos, valueRad, scale); } @@ -713,8 +776,8 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write(autoTemp); - msg.Write(PowerOn); + msg.WriteBoolean(autoTemp); + msg.WriteBoolean(PowerOn); msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index ca210f1fb..188c75cf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -140,8 +140,7 @@ namespace Barotrauma.Items.Components set; } - private bool AllowUsingMineralScanner => - HasMineralScanner && !isConnectedToSteering; + public override bool RecreateGUIOnResolutionChange => true; partial void InitProjSpecific(ContentXElement element) { @@ -195,7 +194,6 @@ namespace Barotrauma.Items.Components protected override void OnResolutionChanged() { - base.OnResolutionChanged(); UpdateGUIElements(); } @@ -218,6 +216,7 @@ namespace Barotrauma.Items.Components var sonarModeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.TopCenter), style: null); SonarModeSwitch = new GUIButton(new RectTransform(new Vector2(0.2f, 1), sonarModeArea.RectTransform), string.Empty, style: "SwitchVertical") { + UserData = UIHighlightAction.ElementId.SonarModeSwitch, Selected = false, Enabled = true, ClickSound = GUISoundType.UISwitch, @@ -240,6 +239,7 @@ namespace Barotrauma.Items.Components passiveTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.TopLeft), TextManager.Get("SonarPassive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { + UserData = UIHighlightAction.ElementId.PassiveSonarIndicator, ToolTip = TextManager.Get("SonarTipPassive"), Selected = true, Enabled = false @@ -247,6 +247,7 @@ namespace Barotrauma.Items.Components activeTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.BottomLeft), TextManager.Get("SonarActive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { + UserData = UIHighlightAction.ElementId.ActiveSonarIndicator, ToolTip = TextManager.Get("SonarTipActive"), Selected = false, Enabled = false @@ -281,9 +282,14 @@ namespace Barotrauma.Items.Components }; new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedControlContainer.RectTransform, Anchor.Center), style: "HorizontalLine") - { UserData = "horizontalline" }; + { + UserData = "horizontalline" + }; - var directionalModeFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); + var directionalModeFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null) + { + UserData = UIHighlightAction.ElementId.DirectionalSonarFrame + }; directionalModeSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), directionalModeFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal") { OnClicked = (button, data) => @@ -302,7 +308,7 @@ namespace Barotrauma.Items.Components TextManager.Get("SonarDirectionalPing"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(directionalModeSwitchText); - if (AllowUsingMineralScanner) + if (HasMineralScanner) { AddMineralScannerSwitchToGUI(); } @@ -336,6 +342,18 @@ namespace Barotrauma.Items.Components sonarView.RectTransform.RelativeOffset = new Vector2(0.13f * GUI.RelativeHorizontalAspectRatio, 0); sonarView.RectTransform.SetPosition(Anchor.BottomRight); } + var handle = GuiFrame.GetChild(); + if (handle != null) + { + handle.RectTransform.Parent = controlContainer.RectTransform; + handle.RectTransform.Resize(Vector2.One); + handle.RectTransform.SetAsFirstChild(); + } + } + + protected override void TryCreateDragHandle() + { + base.TryCreateDragHandle(); } private void SetPingDirection(Vector2 direction) @@ -373,7 +391,7 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); - if (AllowUsingMineralScanner && mineralScannerSwitch == null) + if (HasMineralScanner && mineralScannerSwitch == null) { AddMineralScannerSwitchToGUI(); GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); @@ -417,12 +435,32 @@ namespace Barotrauma.Items.Components unsentChanges = true; correctionTimer = CorrectionDelay; } + return true; } }; var mineralScannerSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), mineralScannerFrame.RectTransform, Anchor.CenterRight), TextManager.Get("SonarMineralScanner"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(mineralScannerSwitchText); + + PreventMineralScannerOverlap(); + } + + private void PreventMineralScannerOverlap() + { + if (item.GetComponent() is { } steering && controlContainer is { } container) + { + int containerBottom = container.Rect.Y + container.Rect.Height, + steeringTop = steering.ControlContainer.Rect.Top; + + int amountRaised = 0; + + while (GetContainerBottom() > steeringTop) { amountRaised++; } + + container.RectTransform.AbsoluteOffset = new Point(0, -amountRaised); + + int GetContainerBottom() => containerBottom - amountRaised; + } } public override void UpdateHUD(Character character, float deltaTime, Camera cam) @@ -502,7 +540,7 @@ namespace Barotrauma.Items.Components Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) < (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2); - if (AllowUsingMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) + if (HasMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) { if (MineralClusters == null) { @@ -985,7 +1023,7 @@ namespace Barotrauma.Items.Components missionIndex++; } - if (AllowUsingMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && + if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && (item.CurrentHull == null || !DetectSubmarineWalls)) { foreach (var c in MineralClusters) @@ -995,6 +1033,19 @@ namespace Barotrauma.Items.Components if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } + + bool disrupted = false; + foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections) + { + float dot = Vector2.Dot(Vector2.Normalize(c.center - transducerCenter), disruptPos); + if (dot > 1.0f - disruptStrength) + { + disrupted = true; + break; + } + } + if (disrupted) { continue; } + DrawMarker(spriteBatch, i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, @@ -1754,17 +1805,17 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write(currentMode == Mode.Active); + msg.WriteBoolean(currentMode == Mode.Active); if (currentMode == Mode.Active) { msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8); - msg.Write(useDirectionalPing); + msg.WriteBoolean(useDirectionalPing); if (useDirectionalPing) { float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } - msg.Write(useMineralScanner); + msg.WriteBoolean(useMineralScanner); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index d85bca980..7f06e5dbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -1,13 +1,11 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Extensions; +using System.Linq; namespace Barotrauma.Items.Components { @@ -25,7 +23,9 @@ namespace Barotrauma.Items.Components private GUITickBox maintainPosTickBox, levelEndTickBox, levelStartTickBox; - private GUIComponent statusContainer, dockingContainer, controlContainer; + private GUIComponent statusContainer, dockingContainer; + + public GUIComponent ControlContainer { get; private set; } private bool dockingNetworkMessagePending; @@ -90,6 +90,24 @@ namespace Barotrauma.Items.Components } } + private bool disableControls; + /// + /// Can be used by status effects to disable all the UI controls + /// + public bool DisableControls + { + get { return disableControls; } + set + { + if (disableControls == value) { return; } + disableControls = value; + UpdateGUIElements(); + } + } + + public override bool RecreateGUIOnResolutionChange => true; + + partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) @@ -112,8 +130,8 @@ namespace Barotrauma.Items.Components protected override void CreateGUI() { - controlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterRight), "ItemUI"); - var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center) + ControlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterRight), "ItemUI"); + var paddedControlContainer = new GUIFrame(new RectTransform(ControlContainer.Rect.Size - GUIStyle.ItemFrameMargin, ControlContainer.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); @@ -121,6 +139,7 @@ namespace Barotrauma.Items.Components var steeringModeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), paddedControlContainer.RectTransform, Anchor.TopLeft), style: null); steeringModeSwitch = new GUIButton(new RectTransform(new Vector2(0.2f, 1), steeringModeArea.RectTransform), string.Empty, style: "SwitchVertical") { + UserData = UIHighlightAction.ElementId.SteeringModeSwitch, Selected = autoPilot, Enabled = true, ClickSound = GUISoundType.UISwitch, @@ -162,6 +181,7 @@ namespace Barotrauma.Items.Components maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") { + UserData = UIHighlightAction.ElementId.MaintainPosTickBox, Enabled = autoPilot, Selected = maintainPos, OnSelected = tickBox => @@ -472,7 +492,6 @@ namespace Barotrauma.Items.Components protected override void OnResolutionChanged() { - base.OnResolutionChanged(); UpdateGUIElements(); } @@ -658,6 +677,11 @@ namespace Barotrauma.Items.Components } } + if (DisableControls) + { + dockingModeEnabled = false; + } + dockingContainer.Visible = DockingModeEnabled; statusContainer.Visible = !DockingModeEnabled; if (!DockingModeEnabled) @@ -745,7 +769,7 @@ namespace Barotrauma.Items.Components iceSpireWarningText.Visible = item.Submarine != null && !pressureWarningText.Visible && showIceSpireWarning && Timing.TotalTime % 1.0f < 0.8f; - if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) + if (!disableControls && Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen && (!GameMain.GameSession?.Campaign?.ShowCampaignUI ?? true) && !GUIMessageBox.MessageBoxes.Any(msgBox => msgBox is GUIMessageBox { MessageBoxType: GUIMessageBox.Type.Default })) @@ -815,7 +839,7 @@ namespace Barotrauma.Items.Components keyboardInput = Vector2.Zero; } - if (!UseAutoDocking) { return; } + if (!UseAutoDocking || DisableControls) { return; } if (checkConnectedPortsTimer <= 0.0f) { @@ -871,13 +895,11 @@ namespace Barotrauma.Items.Components posToMaintain = item.Submarine.WorldPosition; } MaintainPos = true; - if (userdata is Vector2) + if (userdata is Vector2 nudgeAmount) { - Sonar sonar = item.GetComponent(); - Vector2 nudgeAmount = (Vector2)userdata; - if (sonar != null) + if (item.GetComponent() is Sonar sonar) { - nudgeAmount *= sonar == null ? 500.0f : 500.0f / sonar.Zoom; + nudgeAmount *= 500.0f / sonar.Zoom; } PosToMaintain += nudgeAmount; } @@ -896,27 +918,27 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write(AutoPilot); - msg.Write(dockingNetworkMessagePending); + msg.WriteBoolean(AutoPilot); + msg.WriteBoolean(dockingNetworkMessagePending); dockingNetworkMessagePending = false; if (!AutoPilot) { //no need to write steering info if autopilot is controlling - msg.Write(steeringInput.X); - msg.Write(steeringInput.Y); + msg.WriteSingle(steeringInput.X); + msg.WriteSingle(steeringInput.Y); } else { - msg.Write(posToMaintain != null); + msg.WriteBoolean(posToMaintain != null); if (posToMaintain != null) { - msg.Write(((Vector2)posToMaintain).X); - msg.Write(((Vector2)posToMaintain).Y); + msg.WriteSingle(((Vector2)posToMaintain).X); + msg.WriteSingle(((Vector2)posToMaintain).Y); } else { - msg.Write(LevelStartSelected); + msg.WriteBoolean(LevelStartSelected); } } } @@ -1000,9 +1022,20 @@ namespace Barotrauma.Items.Components steeringModeSwitch.Selected = AutoPilot; autopilotIndicator.Selected = AutoPilot; manualPilotIndicator.Selected = !AutoPilot; - maintainPosTickBox.Enabled = AutoPilot; - levelEndTickBox.Enabled = AutoPilot; - levelStartTickBox.Enabled = AutoPilot; + if (DisableControls) + { + steeringModeSwitch.Enabled = false; + maintainPosTickBox.Enabled = false; + levelEndTickBox.Enabled = false; + levelStartTickBox.Enabled = false; + } + else + { + steeringModeSwitch.Enabled = true; + maintainPosTickBox.Enabled = AutoPilot; + levelEndTickBox.Enabled = AutoPilot; + levelStartTickBox.Enabled = AutoPilot; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index cd6883441..72070c051 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -83,7 +83,8 @@ namespace Barotrauma.Items.Components } }; rechargeSpeedSlider.Bar.RectTransform.MaxSize = new Point(rechargeSpeedSlider.Bar.Rect.Height); - + rechargeSpeedSlider.Frame.UserData = UIHighlightAction.ElementId.RechargeSpeedSlider; + // lower area -------------------------- var chargeTextContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs index e3d4e35c4..ae0b6e456 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs @@ -1,8 +1,4 @@ -using Barotrauma.Sounds; -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Items.Components +namespace Barotrauma.Items.Components { partial class Powered : ItemComponent { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 156e1afc1..afaeb2dca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components private GUILayoutGroup extraButtonContainer; + private GUIComponent skillTextContainer; + private readonly List particleEmitters = new List(); //the corresponding particle emitter is active when the condition is within this range private readonly List particleEmitterConditionRanges = new List(); @@ -59,7 +61,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { if (item.HiddenInGame) { return false; } - if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) { return false; } + if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } float defaultMaxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; @@ -110,6 +112,7 @@ namespace Barotrauma.Items.Components if (GuiFrame != null) { GuiFrame.ClearChildren(); + TryCreateDragHandle(); CreateGUI(); } } @@ -131,9 +134,10 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); + skillTextContainer = paddedFrame; for (int i = 0; i < requiredSkills.Count; i++) { - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillTextContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), font: GUIStyle.SmallFont) { @@ -161,6 +165,7 @@ namespace Barotrauma.Items.Components repairingText = TextManager.Get("Repairing"); RepairButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), progressBarHolder.RectTransform, Anchor.TopCenter), repairButtonText) { + UserData = UIHighlightAction.ElementId.RepairButton, OnClicked = (btn, obj) => { requestStartFixAction = FixActions.Repair; @@ -265,7 +270,7 @@ namespace Barotrauma.Items.Components } } - if (CurrentFixer != null && CurrentFixer.SelectedConstruction == item) + if (CurrentFixer != null && CurrentFixer.SelectedItem == item) { if (repairSoundChannel == null || !repairSoundChannel.IsPlaying) { @@ -353,24 +358,16 @@ namespace Barotrauma.Items.Components tinkerButtonText : tinkeringText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); - System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); + //System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible; extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.Visible; - foreach (GUIComponent c in GuiFrame.GetChild(0).Children) + foreach (GUIComponent c in skillTextContainer.Children) { - if (!(c.UserData is Skill skill)) continue; - + if (c.UserData is not Skill skill) { continue; } GUITextBlock textBlock = (GUITextBlock)c; - if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) - { - textBlock.TextColor = GUIStyle.Red; - } - else - { - textBlock.TextColor = Color.White; - } + textBlock.TextColor = character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier) ? GUIStyle.Red : GUIStyle.TextColorNormal; } } @@ -450,7 +447,7 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)requestStartFixAction, 0, 2); - msg.Write(qteSuccess); + msg.WriteBoolean(qteSuccess); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index 89d1a7583..80fbd44db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -94,7 +94,6 @@ namespace Barotrauma.Items.Components protected override void OnResolutionChanged() { - base.OnResolutionChanged(); OnItemLoadedProjSpecific(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 8d5d16766..022972368 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -21,12 +21,13 @@ namespace Barotrauma.Items.Components public float FlashTimer { get; private set; } public static Wire DraggingConnected { get; private set; } - public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Character character) + public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character) { if (DraggingConnected?.Item?.Removed ?? true) { DraggingConnected = null; } + Rectangle panelRect = panel.GuiFrame.Rect; int x = panelRect.X, y = panelRect.Y; int width = panelRect.Width, height = panelRect.Height; @@ -131,7 +132,10 @@ namespace Barotrauma.Items.Components { if (mouseInRect) { - DrawWire(spriteBatch, DraggingConnected, PlayerInput.MousePosition, new Vector2(x + width / 2, y + height - 10), null, panel, ""); + Vector2 wireDragPos = new Vector2( + MathHelper.Clamp(PlayerInput.MousePosition.X, dragArea.X, dragArea.Right), + MathHelper.Clamp(PlayerInput.MousePosition.Y, dragArea.Y, dragArea.Bottom)); + DrawWire(spriteBatch, DraggingConnected, wireDragPos, new Vector2(x + width / 2, y + height - 10), null, panel, ""); } panel.TriggerRewiringSound(); @@ -317,6 +321,7 @@ namespace Barotrauma.Items.Components bool mouseOn = canDrag && + !(GUI.MouseOn is GUIDragHandle) && ((PlayerInput.MousePosition.X > Math.Min(start.X, end.X) && PlayerInput.MousePosition.X < Math.Max(start.X, end.X) && MathUtils.LineToPointDistanceSquared(start, end, PlayerInput.MousePosition) < 36) || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 541c54a63..63560b853 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -27,16 +26,25 @@ namespace Barotrauma.Items.Components private Point originalMaxSize; private Vector2 originalRelativeSize; + private GUIComponent dragArea; + + public override bool RecreateGUIOnResolutionChange => true; + partial void InitProjSpecific() { if (GuiFrame == null) { return; } originalMaxSize = GuiFrame.RectTransform.MaxSize; originalRelativeSize = GuiFrame.RectTransform.RelativeSize; CheckForLabelOverlap(); - new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) + var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) { UserData = this }; + content.RectTransform.SetAsFirstChild(); + + //prevents inputs from going through the GUICustomComponent to the drag handle + dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) + { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); } public void TriggerRewiringSound() @@ -62,7 +70,7 @@ namespace Barotrauma.Items.Components } rewireSoundTimer -= deltaTime; - if (user != null && user.SelectedConstruction == item && rewireSoundTimer > 0.0f) + if (user != null && user.SelectedItem == item && rewireSoundTimer > 0.0f) { if (rewireSoundChannel == null || !rewireSoundChannel.IsPlaying) { @@ -85,12 +93,12 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - return character == Character.Controlled && character == user && character.SelectedConstruction == item; + return character == Character.Controlled && character == user && character.SelectedItem == item; } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - if (character != Character.Controlled || character != user || character.SelectedConstruction != item) { return; } + if (character != Character.Controlled || character != user || character.SelectedItem != item) { return; } if (HighlightedWire != null) { @@ -105,7 +113,7 @@ namespace Barotrauma.Items.Components if (user != Character.Controlled || user == null) { return; } HighlightedWire = null; - Connection.DrawConnections(spriteBatch, this, user); + Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user); foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) { @@ -115,7 +123,6 @@ namespace Barotrauma.Items.Components protected override void OnResolutionChanged() { - base.OnResolutionChanged(); if (GuiFrame == null) { return; } CheckForLabelOverlap(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 706105771..17b151087 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Items.Components private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); + public override bool RecreateGUIOnResolutionChange => true; + partial void InitProjSpecific() { CreateGUI(); @@ -133,7 +135,7 @@ namespace Barotrauma.Items.Components } else { - DebugConsole.ShowError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); + DebugConsole.LogError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); } if (numberInput != null) { @@ -354,29 +356,29 @@ namespace Barotrauma.Items.Components { if (!element.IsNumberInput) { - msg.Write(((GUITextBox)uiElements[i]).Text); + msg.WriteString(((GUITextBox)uiElements[i]).Text); } else { switch (element.NumberType) { case NumberType.Float: - msg.Write(((GUINumberInput)uiElements[i]).FloatValue.ToString()); + msg.WriteString(((GUINumberInput)uiElements[i]).FloatValue.ToString()); break; case NumberType.Int: default: - msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + msg.WriteString(((GUINumberInput)uiElements[i]).IntValue.ToString()); break; } } } else if (element.ContinuousSignal) { - msg.Write(((GUITickBox)uiElements[i]).Selected); + msg.WriteBoolean(((GUITickBox)uiElements[i]).Selected); } else { - msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 980a6f256..ec3054df9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -149,7 +149,7 @@ namespace Barotrauma.Items.Components { if (TryExtractEventData(extraData, out ClientEventData eventData)) { - msg.Write(eventData.Text); + msg.WriteString(eventData.Text); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index e0b3a75f0..17922b0b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components Wire equippedWire = Character.Controlled.HeldItems.FirstOrDefault(it => it.GetComponent() != null)?.GetComponent(); if (equippedWire != null && GUI.MouseOn == null) { - if (PlayerInput.PrimaryMouseButtonClicked() && Character.Controlled.SelectedConstruction == null) + if (PlayerInput.PrimaryMouseButtonClicked() && Character.Controlled.SelectedItem == null) { equippedWire.Use(1.0f, Character.Controlled); } @@ -603,11 +603,11 @@ namespace Barotrauma.Items.Components { var eventData = ExtractEventData(extraData); int nodeCount = eventData.NodeCount; - msg.Write((byte)nodeCount); + msg.WriteByte((byte)nodeCount); if (nodeCount > 0) { - msg.Write(nodes.Last().X); - msg.Write(nodes.Last().Y); + msg.WriteSingle(nodes.Last().X); + msg.WriteSingle(nodes.Last().Y); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 919eae413..03e4e2eff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -5,9 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -94,14 +92,6 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(false, IsPropertySaveable.No, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] - public bool UseFiringOffsetForMuzzleFlash - { - get; - private set; - } - - public Vector2 DrawSize { get @@ -188,7 +178,7 @@ namespace Barotrauma.Items.Components recoilTimer /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed); } PlaySound(ActionType.OnUse); - Vector2 particlePos = GetRelativeFiringPosition(UseFiringOffsetForMuzzleFlash); + Vector2 particlePos = GetRelativeFiringPosition(); foreach (ParticleEmitter emitter in particleEmitters) { emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation); @@ -248,7 +238,6 @@ namespace Barotrauma.Items.Components { moveSoundChannel.FadeOutAndDispose(); moveSoundChannel = null; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 8918490f2..4d4893e4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Items.Components { foreach (DamageModifier damageModifier in damageModifiers) { - if (MathUtils.NearlyEqual(damageModifier.DamageMultiplier, 1f)) + if (MathUtils.NearlyEqual(damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier, 1f)) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 9466e4377..eeeb4f627 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write((byte)allowOutpostAutoDocking); + msg.WriteByte((byte)allowOutpostAutoDocking); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index d4ad96681..7858e3154 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -364,7 +364,7 @@ namespace Barotrauma get { return Character.Controlled != null && - Character.Controlled.SelectedConstruction == null && + !Character.Controlled.HasSelectedAnyItem && CharacterHealth.OpenHealthWindow == null && DraggingItems.Any(); } @@ -924,9 +924,9 @@ namespace Barotrauma } } - if (Character.Controlled.SelectedConstruction != null) + if (Character.Controlled.SelectedItem != null) { - foreach (var ic in Character.Controlled.SelectedConstruction.ActiveHUDs) + foreach (var ic in Character.Controlled.SelectedItem.ActiveHUDs) { var itemContainer = ic as ItemContainer; if (itemContainer?.Inventory?.visualSlots == null) { continue; } @@ -1003,9 +1003,9 @@ namespace Barotrauma } } - if (character.SelectedConstruction != null) + if (character.SelectedItem != null) { - foreach (var ic in character.SelectedConstruction.ActiveHUDs) + foreach (var ic in character.SelectedItem.ActiveHUDs) { var itemContainer = ic as ItemContainer; if (itemContainer?.Inventory?.visualSlots == null) { continue; } @@ -1147,15 +1147,16 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); + bool mouseOnPortrait = CharacterHUD.MouseOnCharacterPortrait(); if (!DetermineMouseOnInventory(ignoreDraggedItem: true) && - CharacterHealth.OpenHealthWindow != null) + (CharacterHealth.OpenHealthWindow != null || mouseOnPortrait)) { bool dropSuccessful = false; foreach (Item item in DraggingItems) { var inventory = item.ParentInventory; var indices = inventory?.FindIndices(item); - dropSuccessful |= CharacterHealth.OpenHealthWindow.OnItemDropped(item, false); + dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait); if (dropSuccessful) { if (indices != null && inventory.visualSlots != null) @@ -1167,7 +1168,6 @@ namespace Barotrauma } break; } - } if (dropSuccessful) { @@ -1341,27 +1341,29 @@ namespace Barotrauma } else { - var rootOwner = (selectedSlot.ParentInventory?.Owner as Item)?.GetRootInventoryOwner(); - if (selectedSlot.ParentInventory?.Owner != Character.Controlled && - selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedCharacter && - selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedConstruction && - !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(selectedSlot.ParentInventory?.Owner) ?? false) && - rootOwner != Character.Controlled && - rootOwner != Character.Controlled.SelectedCharacter && - rootOwner != Character.Controlled.SelectedConstruction && - !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(rootOwner) ?? false)) + static bool OwnerInaccessible(Entity owner) => + owner != Character.Controlled && + owner != Character.Controlled.SelectedCharacter && + owner != Character.Controlled.SelectedItem && + (Character.Controlled.SelectedItem == null || !Character.Controlled.SelectedItem.linkedTo.Contains(owner)); + + Entity owner = selectedSlot.ParentInventory?.Owner; + Entity rootOwner = (owner as Item)?.GetRootInventoryOwner(); + if (OwnerInaccessible(owner) && (rootOwner == owner || OwnerInaccessible(rootOwner))) { return false; } - var parentItem = (selectedSlot?.ParentInventory?.Owner as Item) ?? selectedSlot?.Item; - if ((parentItem?.GetRootInventoryOwner() is Character ownerCharacter) && - ownerCharacter == Character.Controlled && - CharacterHealth.OpenHealthWindow?.Character != ownerCharacter && - ownerCharacter.Inventory.IsInLimbSlot(parentItem, InvSlotType.HealthInterface) && - Screen.Selected != GameMain.SubEditorScreen) + Item parentItem = (owner as Item) ?? selectedSlot?.Item; + if (parentItem?.GetRootInventoryOwner() is Character ownerCharacter) { - highlightedSubInventorySlots.RemoveWhere(s => s.Item == parentItem); - return false; + if (ownerCharacter == Character.Controlled && + CharacterHealth.OpenHealthWindow?.Character != ownerCharacter && + ownerCharacter.Inventory.IsInLimbSlot(parentItem, InvSlotType.HealthInterface) && + Screen.Selected != GameMain.SubEditorScreen) + { + highlightedSubInventorySlots.RemoveWhere(s => s.Item == parentItem); + return false; + } } } return true; @@ -1442,7 +1444,10 @@ namespace Barotrauma float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); Vector2 itemPos = PlayerInput.MousePosition; - bool mouseOnHealthInterface = CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement && DraggingItems.Any(it => it.UseInHealthInterface); + bool mouseOnHealthInterface = + (CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement)|| + CharacterHUD.MouseOnCharacterPortrait(); + mouseOnHealthInterface = mouseOnHealthInterface && DraggingItems.Any(it => it.UseInHealthInterface); if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { @@ -1451,13 +1456,25 @@ namespace Barotrauma Character.Controlled.FocusedItem != null ? TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes) : TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); - int textWidth = (int)Math.Max(GUIStyle.Font.MeasureString(DraggingItems.First().Name).X, GUIStyle.SmallFont.MeasureString(toolTip).X); + + Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name); + Vector2 toolTipSize = GUIStyle.SmallFont.MeasureString(toolTip); + int textWidth = (int)Math.Max(nameSize.X, toolTipSize.X); int textSpacing = (int)(15 * GUI.Scale); - Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); + + Vector2 textPos = itemPos; + int textDir = textPos.X + textWidth * 1.5f > GameMain.GraphicsWidth ? -1 : 1; + int textOffset = textDir == 1 ? 0 : -1; + textPos += new Vector2((iconSize / 2 + textSpacing) * textDir, 0); + + Point shadowPadding = new Point(40, 20).Multiply(GUI.Scale); + Point shadowSize = new Point(iconSize + textWidth + textSpacing, iconSize) + shadowPadding.Multiply(2); + shadowSprite.Draw(spriteBatch, - new Rectangle(itemPos.ToPoint() - new Point(iconSize / 2) - shadowBorders, new Point(iconSize + textWidth + textSpacing, iconSize) + shadowBorders.Multiply(2)), Color.Black * 0.8f); - GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y - iconSize / 2), DraggingItems.First().Name, Color.White); - GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y), toolTip, + new Rectangle(itemPos.ToPoint() - new Point((iconSize / 2 - shadowPadding.X) * textDir - shadowSize.X * textOffset, iconSize / 2 + shadowPadding.Y), shadowSize), Color.Black * 0.8f); + + GUI.DrawString(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White); + GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip, color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, font: GUIStyle.SmallFont); } @@ -1725,7 +1742,8 @@ namespace Barotrauma if (inventory != null && !inventory.Locked && Character.Controlled?.Inventory == inventory && - slot.InventoryKeyIndex != -1) + slot.InventoryKeyIndex != -1 && + slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 32610f313..0c35c29eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -118,7 +118,7 @@ namespace Barotrauma return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } - public Color GetSpriteColor() + public Color GetSpriteColor(bool withHighlight = false) { Color color = spriteColor; if (Prefab.UseContainedSpriteColor && ownInventory != null) @@ -129,6 +129,17 @@ namespace Barotrauma break; } } + if (withHighlight) + { + if (IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen) + { + color = GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float)byte.MaxValue, 0.1f); + } + else if (IsHighlighted && HighlightColor.HasValue) + { + color = Color.Lerp(color, HighlightColor.Value, (MathF.Sin((float)Timing.TotalTime * 3.0f) + 1.0f) / 2.0f); + } + } return color; } @@ -281,9 +292,7 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); - - //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); + Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; @@ -569,6 +578,39 @@ namespace Barotrauma } } + partial void Splash() + { + if (body == null || CurrentHull == null) { return; } + //create a splash particle + float massFactor = MathHelper.Clamp(body.Mass, 0.5f, 20.0f); + for (int i = 0; i < MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y), 1.0f, 10.0f); i++) + { + var splash = GameMain.ParticleManager.CreateParticle("watersplash", + new Vector2(WorldPosition.X, CurrentHull.WorldSurface), + new Vector2(0.0f, Math.Abs(-body.LinearVelocity.Y * massFactor)) + Rand.Vector(Math.Abs(body.LinearVelocity.Y * 10)), + Rand.Range(0.0f, MathHelper.TwoPi), CurrentHull); + if (splash != null) + { + splash.Size *= MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y) * 0.1f * massFactor, 1.0f, 4.0f); + } + } + GameMain.ParticleManager.CreateParticle("bubbles", + new Vector2(WorldPosition.X, CurrentHull.WorldSurface), + body.LinearVelocity * massFactor, + 0.0f, CurrentHull); + + //create a wave + if (body.LinearVelocity.Y < 0.0f) + { + int n = (int)((Position.X - CurrentHull.Rect.X) / Hull.WaveWidth); + if (n >= 0 && n < currentHull.WaveVel.Length) + { + CurrentHull.WaveVel[n] += MathHelper.Clamp(body.LinearVelocity.Y * massFactor, -5.0f, 5.0f); + } + } + SoundPlayer.PlaySplashSound(WorldPosition, Math.Abs(body.LinearVelocity.Y) + Rand.Range(-10.0f, -5.0f)); + } + public void CheckNeedsSoundUpdate(ItemComponent ic) { if (ic.NeedsSoundUpdate()) @@ -790,7 +832,7 @@ namespace Barotrauma reloadTextureButton.OnClicked += (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(updateAllSprites: true); + Sprite.ReloadTexture(); return true; }; } @@ -976,13 +1018,13 @@ namespace Barotrauma /// /// Reposition currently active item interfaces to make sure they don't overlap with each other /// - private void SetHUDLayout() + private void SetHUDLayout(bool ignoreLocking = false) { //reset positions first List elementsToMove = new List(); if (editingHUD != null && editingHUD.UserData == this && - ((HasInGameEditableProperties && Character.Controlled?.SelectedConstruction == this) || Screen.Selected == GameMain.SubEditorScreen)) + ((HasInGameEditableProperties && Character.Controlled?.SelectedItem == this) || Screen.Selected == GameMain.SubEditorScreen)) { elementsToMove.Add(editingHUD); } @@ -991,6 +1033,7 @@ namespace Barotrauma foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; } + if (!ignoreLocking && ic.LockGuiFramePosition) { continue; } //if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; @@ -1015,11 +1058,7 @@ namespace Barotrauma disallowedAreas.Add(editor.ToggleEntityMenuButton.Rect); } - GUI.PreventElementOverlap(elementsToMove, disallowedAreas, - new Rectangle( - 0, 20, - GameMain.GraphicsWidth, - HUDLayoutSettings.InventoryTopY > 0 ? HUDLayoutSettings.InventoryTopY - 40 : GameMain.GraphicsHeight - 80)); + GUI.PreventElementOverlap(elementsToMove, disallowedAreas, clampArea: HUDLayoutSettings.ItemHUDArea); //System.Diagnostics.Debug.WriteLine("after: " + elementsToMove[0].Rect.ToString() + " " + elementsToMove[1].Rect.ToString()); foreach (ItemComponent ic in activeHUDs) @@ -1042,7 +1081,7 @@ namespace Barotrauma public void UpdateHUD(Camera cam, Character character, float deltaTime) { bool editingHUDCreated = false; - if ((HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) || + if ((HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped)) || Screen.Selected == GameMain.SubEditorScreen) { GUIComponent prevEditingHUD = editingHUD; @@ -1126,7 +1165,7 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter != character && - otherCharacter.SelectedConstruction == this) + otherCharacter.SelectedItem == this) { ItemInUseWarning.Visible = true; if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); } @@ -1145,7 +1184,7 @@ namespace Barotrauma public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) { - if (HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) + if (HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped)) { DrawEditing(spriteBatch, cam); } @@ -1215,6 +1254,7 @@ namespace Barotrauma if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } + if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; } Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) @@ -1238,6 +1278,24 @@ namespace Barotrauma return texts; } + public void ForceHUDLayoutUpdate(bool ignoreLocking = false) + { + foreach (ItemComponent ic in activeHUDs) + { + if (ic.GuiFrame == null || !ic.CanBeSelected) { continue; } + ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; + if (ic.UseAlternativeLayout) + { + ic.AlternativeLayout?.ApplyTo(ic.GuiFrame.RectTransform); + } + else + { + ic.DefaultLayout?.ApplyTo(ic.GuiFrame.RectTransform); + } + } + SetHUDLayout(ignoreLocking); + } + public override void AddToGUIUpdateList(int order = 0) { if (Screen.Selected is SubEditorScreen) @@ -1246,15 +1304,15 @@ namespace Barotrauma } else { - if (HasInGameEditableProperties && Character.Controlled != null && (Character.Controlled.SelectedConstruction == this || EditableWhenEquipped)) + if (HasInGameEditableProperties && Character.Controlled != null && (Character.Controlled.SelectedItem == this || EditableWhenEquipped)) { if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } } - if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this && GetComponent() == null) + if (Character.Controlled != null && Character.Controlled.SelectedItem != this && GetComponent() == null) { - if (Character.Controlled.SelectedConstruction?.GetComponent()?.TargetItem != this && + if (Character.Controlled.SelectedItem?.GetComponent()?.TargetItem != this && !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) { return; @@ -1423,8 +1481,8 @@ namespace Barotrauma case TreatmentEventData treatmentEventData: Character targetCharacter = treatmentEventData.TargetCharacter; - msg.Write(targetCharacter.ID); - msg.Write(treatmentEventData.LimbIndex); + msg.WriteUInt16(targetCharacter.ID); + msg.WriteByte(treatmentEventData.LimbIndex); break; case ChangePropertyEventData changePropertyEventData: WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: true); @@ -1432,7 +1490,7 @@ namespace Barotrauma break; case CombineEventData combineEventData: Item combineTarget = combineEventData.CombineTarget; - msg.Write(combineTarget.ID); + msg.WriteUInt16(combineTarget.ID); break; default: throw error($"Unsupported event type {eventData.GetType().Name}"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs index 20ab00f8a..35e6f8385 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -17,7 +16,7 @@ namespace Barotrauma { if (PlayerInput.KeyHit(InputType.Select)) { - Character.Controlled.SelectedConstruction = null; + Character.Controlled.SelectedItem = null; } }*/ } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 864384257..8c6aed81b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -222,13 +222,14 @@ namespace Barotrauma if (!ResizeHorizontal && !ResizeVertical) { - if (PlayerInput.PrimaryMouseButtonClicked()) + if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { var item = new Item(new Rectangle((int)position.X, (int)position.Y, (int)(Sprite.size.X * Scale), (int)(Sprite.size.Y * Scale)), this, Submarine.MainSub) { Submarine = Submarine.MainSub }; item.SetTransform(ConvertUnits.ToSimUnits(Submarine.MainSub == null ? item.Position : item.Position - Submarine.MainSub.Position), 0.0f); + item.GetComponent()?.RefreshLinkedGap(); item.FindHull(); item.Submarine = Submarine.MainSub; @@ -252,7 +253,7 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) placePosition = position; + if (PlayerInput.PrimaryMouseButtonHeld() && GUI.MouseOn == null) { placePosition = position; } } else { @@ -270,11 +271,10 @@ namespace Barotrauma item.SetTransform(ConvertUnits.ToSimUnits(Submarine.MainSub == null ? item.Position : item.Position - Submarine.MainSub.Position), 0.0f); item.FindHull(); - //selected = null; + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { item }, false)); + return; } - - position = placePosition; } } @@ -282,22 +282,12 @@ namespace Barotrauma { potentialContainer.IsHighlighted = true; } - - - //if (PlayerInput.GetMouseState.RightButton == ButtonState.Pressed) selected = null; - } public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - if (PlayerInput.SecondaryMouseButtonClicked()) - { - Selected = null; - return; - } - if (!ResizeHorizontal && !ResizeVertical) { Sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + Sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 8da238592..77189d2d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -264,10 +264,6 @@ namespace Barotrauma { Vector2 velocity = flowForce; if (!IsHorizontal) - { - velocity.X = Rand.Range(-100.0f, 100.0f) * open; - } - else { velocity.X *= Rand.Range(1.0f, 3.0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 01bab48a4..7024d24bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -611,7 +611,7 @@ namespace Barotrauma case DecalEventData decalEventData: var decal = decalEventData.Decal; int decalIndex = decals.IndexOf(decal); - msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); + msg.WriteByte((byte)(decalIndex < 0 ? 255 : decalIndex)); msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 25e6141c5..278e3a36c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -666,7 +666,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } dPos.Y += 48; - GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatZeroDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); } } } @@ -981,7 +981,7 @@ namespace Barotrauma Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) { - GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + (int)connection.Difficulty + ")", Color.White); + GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + connection.Difficulty.FormatSingleDecimal() + ")", Color.White); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 95d24132c..a33dfa34a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -21,6 +21,7 @@ namespace Barotrauma private static float keyDelay; public static Vector2 StartMovingPos => startMovingPos; + public static Vector2 SelectionPos => selectionPos; public event Action Resized; @@ -128,7 +129,9 @@ namespace Barotrauma return; } - if (GUI.MouseOn != null || !PlayerInput.MouseInsideWindow) + if (startMovingPos == Vector2.Zero + && selectionPos == Vector2.Zero + && (GUI.MouseOn != null || !PlayerInput.MouseInsideWindow)) { if (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn))) @@ -819,7 +822,7 @@ namespace Barotrauma selectionPos = Vector2.Zero; } } - if (selectionPos != null && selectionPos != Vector2.Zero) + if (selectionPos != Vector2.Zero) { var (sizeX, sizeY) = selectionSize; var (posX, posY) = selectionPos; @@ -1097,6 +1100,10 @@ namespace Barotrauma resizeDirY = y; resizing = true; startMovingPos = Vector2.Zero; + foreach (var mapEntity in mapEntityList) + { + if (mapEntity != this) { mapEntity.isHighlighted = false; } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index daf65126a..d70dc4087 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -21,7 +21,7 @@ namespace Barotrauma { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - if (PlayerInput.PrimaryMouseButtonHeld()) placePosition = position; + if (PlayerInput.PrimaryMouseButtonHeld() && GUI.MouseOn == null) placePosition = position; } else { @@ -39,7 +39,7 @@ namespace Barotrauma newRect.Location -= MathUtils.ToPoint(Submarine.MainSub.Position); } - if (PlayerInput.PrimaryMouseButtonReleased()) + if (PlayerInput.PrimaryMouseButtonReleased() && GUI.MouseOn == null) { CreateInstance(newRect); placePosition = Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 596935d5b..b41a0f0a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -37,17 +37,6 @@ namespace Barotrauma } } -#if DEBUG - [Editable, Serialize("", IsPropertySaveable.Yes)] -#else - [Serialize("", IsPropertySaveable.Yes)] -#endif - public string SpecialTag - { - get; - set; - } - partial void InitProjSpecific() { Prefab.Sprite?.EnsureLazyLoaded(); @@ -174,7 +163,7 @@ namespace Barotrauma OnClicked = (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(updateAllSprites: true); + Sprite.ReloadTexture(); return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index ee708d2ef..f031e7fab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -27,9 +27,10 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && GUI.MouseOn == null) + { placePosition = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - + } newRect.X = (int)position.X; newRect.Y = (int)position.Y; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 463d94253..a08893601 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -80,7 +80,8 @@ namespace Barotrauma { UserData = "descriptionbox", ScrollBarVisible = true, - Spacing = 5 + Spacing = 5, + CurrentSelectMode = GUIListBox.SelectMode.None }; GUIFont font = parent.Rect.Width < 350 ? GUIStyle.SmallFont : GUIStyle.Font; @@ -92,22 +93,33 @@ namespace Barotrauma { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; - LocalizedString className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !HasTag(SubmarineTag.Shuttle) ? + TextManager.GetWithVariables("submarine.classandtier", + ("[class]", TextManager.Get($"submarineclass.{SubmarineClass}")), + ("[tier]", TextManager.Get($"submarinetier.{Tier}"))) : + TextManager.Get("shuttle"); int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; - int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); + int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); GUITextBlock submarineNameText = null; GUITextBlock submarineClassText = null; if (includeTitle) { int nameHeight = (int)GUIStyle.LargeFont.MeasureString(DisplayName, true).Y; - submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false }; + submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) + { + CanBeFocused = false + }; submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); } if (includeClass) { - submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) + { + ToolTip = TextManager.Get("submarinetierandclass.description")+"\n\n"+ TextManager.Get($"submarineclass.{SubmarineClass}.description") + }; + submarineClassText.HoverColor = Color.Transparent; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e72fc7fb1..e547ec854 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -144,10 +144,11 @@ namespace Barotrauma specsContainer = new GUIListBox(new RectTransform(new Vector2(0.4f, 1f), innerPadded.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(0.015f, 0.07f) }) { + CurrentSelectMode = GUIListBox.SelectMode.None, Color = Color.Black * 0.65f, ScrollBarEnabled = false, ScrollBarVisible = false, - Spacing = 5 + Spacing = GUI.IntScale(5) }; subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); int width = specsContainer.Rect.Width; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.Internal.cs b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.Internal.cs index 6e6ef4ec2..56e1509e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.Internal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.Internal.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Barotrauma.Media { - public partial class Video : IDisposable + partial class Video : IDisposable { private static class Internal { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs index eaf0966b8..e22d634c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs @@ -11,7 +11,7 @@ using Barotrauma.Sounds; namespace Barotrauma.Media { - public partial class Video : IDisposable + partial class Video : IDisposable { private static Internal.EventCallback VideoFrameCallback; private static Internal.EventCallback VideoAudioCallback; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 1e112d1df..94e3da6c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -1,60 +1,54 @@ -using Barotrauma.Steam; +#nullable enable using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Barotrauma.Networking { partial class BannedPlayer { - public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID, string reason, DateTime? expiration) + public BannedPlayer( + UInt32 uniqueIdentifier, + string name, + Either addressOrAccountId, + string reason, + DateTime? expiration) { this.Name = name; - this.EndPoint = endPoint; - this.SteamID = steamID; - ParseEndPointAsSteamId(); - this.IsRangeBan = isRangeBan; + this.AddressOrAccountId = addressOrAccountId; this.UniqueIdentifier = uniqueIdentifier; this.Reason = reason; this.ExpirationTime = expiration; } } - public partial class BanList + partial class BanList { - private GUIComponent banFrame; + public GUIComponent? BanFrame { get; private set; } - public GUIComponent BanFrame - { - get { return banFrame; } - } - - public List localRemovedBans = new List(); - public List localRangeBans = new List(); + public List localRemovedBans = new List(); private void RecreateBanFrame() { - if (banFrame != null) + if (BanFrame != null) { - var parent = banFrame.Parent; - parent.RemoveChild(banFrame); + var parent = BanFrame.Parent; + parent.RemoveChild(BanFrame); CreateBanFrame(parent); } } public GUIComponent CreateBanFrame(GUIComponent parent) { - banFrame = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)); + BanFrame = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)); foreach (BannedPlayer bannedPlayer in bannedPlayers) { if (localRemovedBans.Contains(bannedPlayer.UniqueIdentifier)) { continue; } - var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)banFrame).Content.RectTransform) { MinSize = new Point(0, 70) }) + var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)BanFrame).Content.RectTransform) { MinSize = new Point(0, 70) }) { - UserData = banFrame + UserData = BanFrame }; var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.85f), playerFrame.RectTransform, Anchor.Center)) @@ -71,54 +65,48 @@ namespace Barotrauma.Networking RelativeSpacing = 0.02f }; - string endPoint = bannedPlayer.EndPoint; - if (localRangeBans.Contains(bannedPlayer.UniqueIdentifier)) endPoint = ToRange(endPoint); - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), topArea.RectTransform), - bannedPlayer.Name + " (" + endPoint + ")"); - textBlock.RectTransform.MinSize = new Point(textBlock.Rect.Width, 0); + var addressOrAccountId = bannedPlayer.AddressOrAccountId; + GUITextBlock textBlock = new GUITextBlock( + new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), + bannedPlayer.Name + " (" + addressOrAccountId + ")") { CanBeFocused = true }; + textBlock.RectTransform.MinSize = new Point( + (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); - if (bannedPlayer.EndPoint.IndexOf(".x") <= -1) - { - var rangeBanButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.4f), topArea.RectTransform), - TextManager.Get("BanRange"), style: "GUIButtonSmall") - { - UserData = bannedPlayer, - OnClicked = RangeBan - }; - } var removeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.4f), topArea.RectTransform), TextManager.Get("BanListRemove"), style: "GUIButtonSmall") { UserData = bannedPlayer, OnClicked = RemoveBan }; - topArea.RectTransform.MinSize = new Point(0, (int)topArea.RectTransform.Children.Max(c => c.Rect.Height * 1.25f)); + topArea.RectTransform.MinSize = new Point(0, (int)(removeButton.Rect.Height * 1.25f)); + + topArea.ForceLayoutRecalculation(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), bannedPlayer.ExpirationTime == null ? TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), font: GUIStyle.SmallFont); + LocalizedString reason = TextManager.GetServerMessage(bannedPlayer.Reason).Fallback(bannedPlayer.Reason); var reasonText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), TextManager.Get("BanReason") + " " + - (string.IsNullOrEmpty(bannedPlayer.Reason) ? TextManager.Get("None") : bannedPlayer.Reason), + (string.IsNullOrEmpty(bannedPlayer.Reason) ? TextManager.Get("None") : reason), font: GUIStyle.SmallFont, wrap: true) { - ToolTip = bannedPlayer.Reason + ToolTip = reason }; paddedPlayerFrame.Recalculate(); - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), ((GUIListBox)banFrame).Content.RectTransform), style: "HorizontalLine"); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), ((GUIListBox)BanFrame).Content.RectTransform), style: "HorizontalLine"); } - return banFrame; + return BanFrame; } private bool RemoveBan(GUIButton button, object obj) { - BannedPlayer banned = obj as BannedPlayer; - if (banned == null) { return false; } + if (!(obj is BannedPlayer banned)) { return false; } localRemovedBans.Add(banned.UniqueIdentifier); RecreateBanFrame(); @@ -127,19 +115,6 @@ namespace Barotrauma.Networking return true; } - - private bool RangeBan(GUIButton button, object obj) - { - BannedPlayer banned = obj as BannedPlayer; - if (banned == null) { return false; } - - localRangeBans.Add(banned.UniqueIdentifier); - RecreateBanFrame(); - - GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Properties); - - return true; - } public void ClientAdminRead(IReadMessage incMsg) { @@ -159,8 +134,7 @@ namespace Barotrauma.Networking for (int i = 0; i < (int)bannedPlayerCount; i++) { string name = incMsg.ReadString(); - UInt16 uniqueIdentifier = incMsg.ReadUInt16(); - bool isRangeBan = incMsg.ReadBoolean(); + UInt32 uniqueIdentifier = incMsg.ReadUInt32(); bool includesExpiration = incMsg.ReadBoolean(); incMsg.ReadPadBits(); @@ -173,45 +147,49 @@ namespace Barotrauma.Networking string reason = incMsg.ReadString(); - string endPoint = ""; - UInt64 steamID = 0; + Either addressOrAccountId; if (isOwner) { - endPoint = incMsg.ReadString(); - steamID = incMsg.ReadUInt64(); + bool isAddress = incMsg.ReadBoolean(); + incMsg.ReadPadBits(); + string str = incMsg.ReadString(); + if (isAddress && Address.Parse(str).TryUnwrap(out var address)) + { + addressOrAccountId = address; + } + else if (AccountId.Parse(str).TryUnwrap(out var accountId)) + { + addressOrAccountId = accountId; + } + else + { + continue; + } } else { - endPoint = "Endpoint concealed by host"; - steamID = 0; + addressOrAccountId = new UnknownAddress(); } - bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID, reason, expiration)); + bannedPlayers.Add(new BannedPlayer(uniqueIdentifier, name, addressOrAccountId, reason, expiration)); } - if (banFrame != null) + if (BanFrame != null) { - var parent = banFrame.Parent; - parent.RemoveChild(banFrame); + var parent = BanFrame.Parent; + parent.RemoveChild(BanFrame); CreateBanFrame(parent); } } public void ClientAdminWrite(IWriteMessage outMsg) { - outMsg.Write((UInt16)localRemovedBans.Count); - foreach (UInt16 uniqueId in localRemovedBans) + outMsg.WriteVariableUInt32((UInt32)localRemovedBans.Count); + foreach (UInt32 uniqueId in localRemovedBans) { - outMsg.Write(uniqueId); - } - - outMsg.Write((UInt16)localRangeBans.Count); - foreach (UInt16 uniqueId in localRangeBans) - { - outMsg.Write(uniqueId); + outMsg.WriteUInt32(uniqueId); } localRemovedBans.Clear(); - localRangeBans.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index a020b263a..8c1319fe5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -8,11 +8,11 @@ namespace Barotrauma.Networking { public virtual void ClientWrite(IWriteMessage msg) { - msg.Write((byte)ClientNetObject.CHAT_MESSAGE); - msg.Write(NetStateID); + msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); - msg.Write(Text); + msg.WriteString(Text); } public static void ClientRead(IReadMessage msg) @@ -35,8 +35,9 @@ namespace Barotrauma.Networking bool hasSenderClient = msg.ReadBoolean(); if (hasSenderClient) { - UInt64 clientId = msg.ReadUInt64(); - senderClient = GameMain.Client.ConnectedClients.Find(c => c.SteamID == clientId || c.ID == clientId); + string userId = msg.ReadString(); + senderClient = GameMain.Client.ConnectedClients.Find(c + => c.SessionOrAccountIdMatches(userId)); if (senderClient != null) { senderName = senderClient.Name; } } bool hasSenderCharacter = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index f9348813e..21b119c40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -31,12 +31,17 @@ namespace Barotrauma.Networking public bool IsOwner; - public bool AllowKicking; public bool IsDownloading; public float Karma; + public bool AllowKicking => + !IsOwner && + !HasPermission(ClientPermissions.Ban) && + !HasPermission(ClientPermissions.Kick) && + !HasPermission(ClientPermissions.Unban); + public void UpdateSoundPosition() { if (VoipSound == null) { return; } @@ -72,8 +77,8 @@ namespace Barotrauma.Networking partial void InitProjSpecific() { VoipQueue = null; VoipSound = null; - if (ID == GameMain.Client.ID) return; - VoipQueue = new VoipQueue(ID, false, true); + if (SessionId == GameMain.Client.SessionId) { return; } + VoipQueue = new VoipQueue(SessionId, canSend: false, canReceive: true); GameMain.Client?.VoipClient?.RegisterQueue(VoipQueue); VoipSound = null; } @@ -134,6 +139,14 @@ namespace Barotrauma.Networking return Permissions.HasFlag(permission); } + public void ResetVotes() + { + for (int i = 0; i < votes.Length; i++) + { + votes[i] = null; + } + } + partial void DisposeProjSpecific() { if (VoipQueue != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1a0efc16c..1809817cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Networking class FileReceiver { public class FileTransferIn : IDisposable - { + { public string FileName { get; @@ -36,7 +36,7 @@ namespace Barotrauma.Networking get; private set; } - + public int LastSeen { get; set; } public FileTransferType FileType @@ -93,6 +93,12 @@ namespace Barotrauma.Networking public int ID; + public const int DataBufferSize = 50; + /// + /// Data that we've ignored because we're waiting for some earlier data. Key = byte offset, value = the actual data + /// + public readonly Dictionary DataBuffer = new Dictionary(); + public FileTransferIn(NetworkConnection connection, string filePath, FileTransferType fileType) { FilePath = filePath; @@ -128,20 +134,25 @@ namespace Barotrauma.Networking bytesToRead -= Received + bytesToRead - FileSize; } - byte[] all = inc.ReadBytes(bytesToRead); - Received += all.Length; - WriteStream.Write(all, 0, all.Length); + ReadBytes(inc.ReadBytes(bytesToRead)); + } + + public void ReadBytes(byte[] data) + { + Received += data.Length; + WriteStream.Write(data, 0, data.Length); int passed = Environment.TickCount - TimeStarted; float psec = passed / 1000.0f; - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.Log($"Received {all.Length} bytes of the file {FileName} ({Received / 1000}/{FileSize / 1000} kB received)"); - } - BytesPerSecond = Received / psec; + var outdatedKeys = DataBuffer.Keys.Where(k => k < Received).ToList(); + foreach (int key in outdatedKeys) + { + DataBuffer.Remove(key); + } + Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving; } @@ -206,7 +217,7 @@ namespace Barotrauma.Networking case (byte)FileTransferMessageType.Initiate: { byte transferId = inc.ReadByte(); - var existingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); + var existingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId); finishedTransfers.RemoveAll(t => t.transferId == transferId); byte fileType = inc.ReadByte(); //ushort chunkLen = inc.ReadUInt16(); @@ -329,7 +340,7 @@ namespace Barotrauma.Networking { byte transferId = inc.ReadByte(); - var activeTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); + var activeTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId); if (activeTransfer == null) { //it's possible for the server to send some extra data @@ -349,6 +360,10 @@ namespace Barotrauma.Networking if (offset != activeTransfer.Received) { activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen); + if (!activeTransfer.DataBuffer.ContainsKey(offset) && activeTransfer.DataBuffer.Count < FileTransferIn.DataBufferSize) + { + activeTransfer.DataBuffer.Add(offset, inc.ReadBytes(bytesToRead)); + } DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})"); GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen); return; @@ -370,7 +385,16 @@ namespace Barotrauma.Networking try { - activeTransfer.ReadBytes(inc, bytesToRead); + activeTransfer.ReadBytes(inc, bytesToRead); + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)"); + } + while (activeTransfer.DataBuffer.TryGetValue(activeTransfer.Received, out byte[] data)) + { + activeTransfer.ReadBytes(data); + DebugConsole.Log($"Read {data.Length} bytes of buffer data of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)"); + } } catch (Exception e) { @@ -406,7 +430,7 @@ namespace Barotrauma.Networking case (byte)FileTransferMessageType.Cancel: { byte transferId = inc.ReadByte(); - var matchingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); + var matchingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId); if (matchingTransfer != null) { new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\"."); @@ -434,7 +458,7 @@ namespace Barotrauma.Networking } if (string.IsNullOrEmpty(fileName) || - fileName.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1) + fileName.IndexOfAny(Path.GetInvalidFileNameCharsCrossPlatform().ToArray()) > -1) { errorMessage = "Illegal characters in file name ''" + fileName + "''"; return false; @@ -470,7 +494,7 @@ namespace Barotrauma.Networking System.IO.Stream stream; try { - stream = SaveUtil.DecompressFiletoStream(fileTransfer.FilePath); + stream = SaveUtil.DecompressFileToStream(fileTransfer.FilePath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 98dec5ac4..8a61e624a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1,44 +1,36 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Barotrauma.Steam; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; -using Barotrauma.IO; -using System.IO.Compression; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework.Input; namespace Barotrauma.Networking { - class GameClient : NetworkMember + sealed class GameClient : NetworkMember { - public override bool IsClient - { - get { return true; } - } + public override bool IsClient => true; + public override bool IsServer => false; - private string name; + public override Voting Voting { get; } private UInt16 nameId = 0; - public string Name - { - get { return name; } - } + public string Name { get; private set; } public string PendingName = string.Empty; public void SetName(string value) { - value = value.Replace(":", "").Replace(";", ""); if (string.IsNullOrEmpty(value)) { return; } - name = value; + Name = value; nameId++; } @@ -47,8 +39,7 @@ namespace Barotrauma.Networking nameId++; } - private ClientPeer clientPeer; - public ClientPeer ClientPeer { get { return clientPeer; } } + public ClientPeer ClientPeer { get; private set; } private GUIMessageBox reconnectBox, waitInServerQueueBox; @@ -63,7 +54,7 @@ namespace Barotrauma.Networking public GUITickBox FollowSubTickBox => cameraFollowsSub; public bool IsFollowSubTickBoxVisible => - gameStarted && Screen.Selected == GameMain.GameScreen && + GameStarted && Screen.Selected == GameMain.GameScreen && cameraFollowsSub != null && cameraFollowsSub.Visible; public CameraTransition EndCinematic; @@ -89,17 +80,12 @@ namespace Barotrauma.Networking public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize; - private byte myID; - private readonly List otherClients; public readonly List ServerSubmarines = new List(); - private string serverIP, serverName; + public string ServerName { get; private set; } - private bool allowReconnect; - private bool requiresPw; - private int pwRetries; private bool canStart; private UInt16 lastSentChatMsgID = 0; //last message this client has successfully sent @@ -108,14 +94,10 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID; - private readonly ClientEntityEventManager entityEventManager; - - private readonly FileReceiver fileReceiver; - #if DEBUG public void PrintReceiverTransters() { - foreach (var transfer in fileReceiver.ActiveTransfers) + foreach (var transfer in FileReceiver.ActiveTransfers) { DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); } @@ -129,10 +111,7 @@ namespace Barotrauma.Networking public LocalizedString TraitorFirstObjective; public TraitorMissionPrefab TraitorMission = null; - public byte ID - { - get { return myID; } - } + public byte SessionId { get; private set; } public VoipClient VoipClient { @@ -140,7 +119,7 @@ namespace Barotrauma.Networking private set; } - public override List ConnectedClients + public override IReadOnlyList ConnectedClients { get { @@ -148,26 +127,30 @@ namespace Barotrauma.Networking } } + public Option Ping + { + get + { + Client selfClient = ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId); + if (selfClient is null || selfClient.Ping == 0) { return Option.None(); } + return Option.Some(selfClient.Ping); + } + } + private readonly List previouslyConnectedClients = new List(); public IEnumerable PreviouslyConnectedClients { get { return previouslyConnectedClients; } } - public FileReceiver FileReceiver - { - get { return fileReceiver; } - } + public readonly FileReceiver FileReceiver; public bool MidRoundSyncing { - get { return entityEventManager.MidRoundSyncing; } + get { return EntityEventManager.MidRoundSyncing; } } - public ClientEntityEventManager EntityEventManager - { - get { return entityEventManager; } - } + public readonly ClientEntityEventManager EntityEventManager; public bool? WaitForNextRoundRespawn { @@ -175,14 +158,10 @@ namespace Barotrauma.Networking set; } - private readonly object serverEndpoint; - private readonly int ownerKey; - private readonly bool steamP2POwner; + private readonly Endpoint serverEndpoint; + private readonly Option ownerKey; - public bool IsServerOwner - { - get { return ownerKey > 0 || steamP2POwner; } - } + public bool IsServerOwner => ownerKey.IsSome(); internal readonly struct PermissionChangedEvent { @@ -198,16 +177,13 @@ namespace Barotrauma.Networking public readonly NamedEvent OnPermissionChanged = new NamedEvent(); - public GameClient(string newName, string ip, UInt64 steamId, string serverName = null, int ownerKey = 0, bool steamP2POwner = false) + public GameClient(string newName, Endpoint endpoint, string serverName, Option ownerKey) { //TODO: gui stuff should probably not be here? this.ownerKey = ownerKey; - this.steamP2POwner = steamP2POwner; roundInitStatus = RoundInitStatus.NotStarted; - allowReconnect = true; - NetStats = new NetStats(); inGameHUD = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) @@ -254,13 +230,13 @@ namespace Barotrauma.Networking { OnClicked = (GUIButton button, object userData) => { - if (serverSettings.ServerLog.LogFrame == null) + if (ServerSettings.ServerLog.LogFrame == null) { - serverSettings.ServerLog.CreateLogFrame(); + ServerSettings.ServerLog.CreateLogFrame(); } else { - serverSettings.ServerLog.LogFrame = null; + ServerSettings.ServerLog.LogFrame = null; GUI.KeyboardDispatcher.Subscriber = null; } return true; @@ -274,31 +250,24 @@ namespace Barotrauma.Networking SetName(newName); - entityEventManager = new ClientEntityEventManager(this); + EntityEventManager = new ClientEntityEventManager(this); - fileReceiver = new FileReceiver(); - fileReceiver.OnFinished += OnFileReceived; - fileReceiver.OnTransferFailed += OnTransferFailed; + FileReceiver = new FileReceiver(); + FileReceiver.OnFinished += OnFileReceived; + FileReceiver.OnTransferFailed += OnTransferFailed; - characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, name, null) + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, Name, originalName: null) { Job = null }; otherClients = new List(); - serverSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false, System.Net.IPAddress.Any); + ServerSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false, System.Net.IPAddress.Any); Voting = new Voting(); - if (steamId == 0) - { - serverEndpoint = ip; - } - else - { - serverEndpoint = steamId; - } - ConnectToServer(serverEndpoint, serverName); + serverEndpoint = endpoint; + InitiateServerJoin(serverName); //ServerLog = new ServerLog(""); @@ -306,7 +275,14 @@ namespace Barotrauma.Networking GameMain.ResetNetLobbyScreen(); } - private void ConnectToServer(object endpoint, string hostName) + public ServerInfo CreateServerInfoFromSettings() + { + var serverInfo = ServerInfo.FromServerConnection(ClientPeer.ServerConnection, ServerSettings); + GameMain.ServerListScreen.UpdateOrAddServerInfo(serverInfo); + return serverInfo; + } + + private void InitiateServerJoin(string hostName) { LastClientListUpdateID = 0; @@ -315,7 +291,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RemovePlayer(c); c.Dispose(); } - ConnectedClients.Clear(); + otherClients.Clear(); chatBox.InputBox.Enabled = false; if (GameMain.NetLobbyScreen?.ChatInput != null) @@ -323,102 +299,36 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.ChatInput.Enabled = false; } - serverName = hostName; + ServerName = hostName; myCharacter = Character.Controlled; ChatMessage.LastID = 0; - clientPeer?.Close(); - clientPeer = null; - object translatedEndpoint = null; - if (endpoint is string hostIP) - { - int port; - string[] address = hostIP.Split(':'); - if (address.Length == 1) - { - serverIP = hostIP; - port = NetConfig.DefaultPort; - } - else - { - serverIP = string.Join(":", address.Take(address.Length - 1)); - if (!int.TryParse(address[address.Length - 1], out port)) - { - DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); - port = NetConfig.DefaultPort; - } - } - - clientPeer = new LidgrenClientPeer(Name); - - System.Net.IPEndPoint IPEndPoint = null; - try - { - IPEndPoint = new System.Net.IPEndPoint(Lidgren.Network.NetUtility.Resolve(serverIP), port); - } - catch - { - new GUIMessageBox(TextManager.Get("CouldNotConnectToServer"), - TextManager.GetWithVariables("InvalidIPAddress", ("[serverip]", serverIP), ("[port]", port.ToString()))); - return; - } - - translatedEndpoint = IPEndPoint; - } - else if (endpoint is UInt64) - { - if (steamP2POwner) - { - clientPeer = new SteamP2POwnerPeer(Name); - } - else - { - clientPeer = new SteamP2PClientPeer(Name); - } - - translatedEndpoint = endpoint; - } - clientPeer.OnDisconnect = OnDisconnect; - clientPeer.OnDisconnectMessageReceived = HandleDisconnectMessage; - clientPeer.OnInitializationComplete = OnConnectionInitializationComplete; - clientPeer.OnRequestPassword = (int salt, int retries) => - { - if (pwRetries != retries) - { - wrongPassword = retries > 0; - requiresPw = true; - } - pwRetries = retries; - }; - clientPeer.OnMessageReceived = ReadDataMessage; - - // Connect client, to endpoint previously requested from user - try - { - clientPeer.Start(translatedEndpoint, ownerKey); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't connect to " + endpoint.ToString() + ". Error message: " + e.Message); - Disconnect(); - chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.ChatInput != null) - { - GameMain.NetLobbyScreen.ChatInput.Enabled = true; - } - GameMain.ServerListScreen.Select(); - return; - } - - updateInterval = new TimeSpan(0, 0, 0, 0, 150); + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + ClientPeer = CreateNetPeer(); + ClientPeer.Start(); CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } + private ClientPeer CreateNetPeer() + { + Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks( + ReadDataMessage, + OnClientPeerDisconnect, + OnConnectionInitializationComplete); + return serverEndpoint switch + { + LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), + SteamP2PEndpoint _ when ownerKey is Some { Value: var key } => new SteamP2POwnerPeer(callbacks, key), + SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), + _ => throw new ArgumentOutOfRangeException() + }; + } + private bool ReturnToPreviousMenu(GUIButton button, object obj) { - Disconnect(); + Quit(); Submarine.Unload(); GameMain.Client = null; @@ -440,26 +350,19 @@ namespace Barotrauma.Networking private bool connectCancelled; private void CancelConnect() { - ChildServerRelay.ShutDown(); - connectCancelled = true; - Disconnect(); + Quit(); } - private bool wrongPassword; - // Before main looping starts, we loop here and wait for approval message private IEnumerable WaitForStartingInfo() { GUI.SetCursorWaiting(); - requiresPw = false; - pwRetries = -1; - connectCancelled = wrongPassword = false; + connectCancelled = false; // When this is set to true, we are approved and ready to go canStart = false; - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 40); - DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 200); // Loop until we are approved LocalizedString connectingText = TextManager.Get("Connecting"); @@ -467,14 +370,13 @@ namespace Barotrauma.Networking { if (reconnectBox == null && waitInServerQueueBox == null) { - string serverDisplayName = serverName; - if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = serverIP; } - if (string.IsNullOrEmpty(serverDisplayName) && clientPeer?.ServerConnection is SteamP2PConnection steamConnection) + string serverDisplayName = ServerName; + if (string.IsNullOrEmpty(serverDisplayName) && ClientPeer?.ServerConnection is SteamP2PConnection steamConnection) { - serverDisplayName = steamConnection.SteamID.ToString(); - if (SteamManager.IsInitialized) + if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) { - string steamUserName = Steamworks.SteamFriends.GetFriendPersonaName(steamConnection.SteamID); + serverDisplayName = steamId.ToString(); + string steamUserName = Steamworks.SteamFriends.GetFriendPersonaName(steamId.Value); if (!string.IsNullOrEmpty(steamUserName) && steamUserName != "[unknown]") { serverDisplayName = steamUserName; @@ -483,12 +385,9 @@ namespace Barotrauma.Networking } if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown").Value; } - reconnectBox = new GUIMessageBox( + CreateReconnectBox( connectingText, - TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName), - new LocalizedString[] { TextManager.Get("Cancel") }); - reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; - reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; + TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName)); } if (reconnectBox != null) @@ -500,82 +399,29 @@ namespace Barotrauma.Networking if (DateTime.Now > timeOut) { - clientPeer?.Close(Lidgren.Network.NetConnection.NoResponseMessage); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer")); + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer")) + { + DisplayInLoadingScreens = true + }; msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); break; } - if (requiresPw && !canStart && !connectCancelled) + if (ClientPeer.WaitingForPassword && !canStart && !connectCancelled) { GUI.ClearCursorWait(); - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); - LocalizedString pwMsg = TextManager.Get("PasswordRequired"); - - var msgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); - 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) }) + while (ClientPeer.WaitingForPassword) { - UserData = "password", - Censor = true - }; - - if (wrongPassword) - { - var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); - incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); - passwordHolder.Recalculate(); - } - - 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]; - okButton.OnClicked += msgBox.Close; - var cancelButton = msgBox.Buttons[1]; - cancelButton.OnClicked += msgBox.Close; - passwordBox.OnEnterPressed += (GUITextBox textBox, string text) => - { - msgBox.Close(); - clientPeer?.SendPassword(passwordBox.Text); - requiresPw = false; - return true; - }; - - okButton.OnClicked += (GUIButton button, object obj) => - { - clientPeer?.SendPassword(passwordBox.Text); - requiresPw = false; - return true; - }; - - cancelButton.OnClicked += (GUIButton button, object obj) => - { - requiresPw = false; - connectCancelled = true; - GameMain.ServerListScreen.Select(); - return true; - }; - yield return CoroutineStatus.Running; - passwordBox.Select(); - - while (GUIMessageBox.MessageBoxes.Contains(msgBox)) - { - if (!requiresPw) - { - msgBox.Close(); - break; - } yield return CoroutineStatus.Running; } } } - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); GUI.ClearCursorWait(); if (connectCancelled) { yield return CoroutineStatus.Success; } @@ -583,7 +429,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public override void Update(float deltaTime) + public void Update(float deltaTime) { #if DEBUG if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.P)) return; @@ -604,7 +450,7 @@ namespace Barotrauma.Networking { if (VoipCapture.Instance.LastEnqueueAudio > DateTime.Now - new TimeSpan(0, 0, 0, 0, milliseconds: 100)) { - var myClient = ConnectedClients.Find(c => c.ID == ID); + var myClient = ConnectedClients.Find(c => c.SessionId == SessionId); if (Screen.Selected == GameMain.NetLobbyScreen) { GameMain.NetLobbyScreen.SetPlayerSpeaking(myClient); @@ -620,8 +466,6 @@ namespace Barotrauma.Networking UpdateHUD(deltaTime); - base.Update(deltaTime); - try { incomingMessagesToProcess.Clear(); @@ -631,7 +475,7 @@ namespace Barotrauma.Networking ReadDataMessage(inc); } pendingIncomingMessages.Clear(); - clientPeer?.Update(deltaTime); + ClientPeer?.Update(deltaTime); } catch (Exception e) { @@ -644,25 +488,24 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError("Error while reading a message from server.", e); - new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))); - Disconnect(); + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))) + { + DisplayInLoadingScreens = true + }; + Quit(); GameMain.ServerListScreen.Select(); return; } - if (!connected) return; + if (!connected) { return; } - if (reconnectBox != null) - { - reconnectBox.Close(); - reconnectBox = null; - } + CloseReconnectBox(); - if (gameStarted && Screen.Selected == GameMain.GameScreen) + if (GameStarted && Screen.Selected == GameMain.GameScreen) { EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); - respawnManager?.Update(deltaTime); + RespawnManager?.Update(deltaTime); if (updateTimer <= DateTime.Now) { @@ -677,7 +520,7 @@ namespace Barotrauma.Networking } } - if (serverSettings.VoiceChatEnabled) + if (ServerSettings.VoiceChatEnabled) { VoipClient?.SendToServer(); } @@ -688,7 +531,7 @@ namespace Barotrauma.Networking { if (ChildServerRelay.Process?.HasExited ?? true) { - Disconnect(); + Quit(); if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) { var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); @@ -701,7 +544,7 @@ namespace Barotrauma.Networking if (updateTimer <= DateTime.Now) { // Update current time - updateTimer = DateTime.Now + updateInterval; + updateTimer = DateTime.Now + UpdateInterval; } } @@ -714,11 +557,12 @@ namespace Barotrauma.Networking GameMain.LuaCs.Networking.NetMessageReceived(inc, header); - if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize && - roundInitStatus == RoundInitStatus.Started && - header != ServerPacketHeader.ENDGAME && - header != ServerPacketHeader.PING_REQUEST && - header != ServerPacketHeader.FILE_TRANSFER) + if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize + && header is not ( + ServerPacketHeader.STARTGAMEFINALIZE + or ServerPacketHeader.ENDGAME + or ServerPacketHeader.PING_REQUEST + or ServerPacketHeader.FILE_TRANSFER)) { //rewind the header byte we just read inc.BitPosition -= 8; @@ -741,10 +585,10 @@ namespace Barotrauma.Networking //allow interpreting this packet break; case ServerPacketHeader.STARTGAME: - gameStarted = true; + GameStarted = true; return; case ServerPacketHeader.ENDGAME: - gameStarted = false; + GameStarted = false; return; default: return; //ignore any other packets @@ -755,15 +599,15 @@ namespace Barotrauma.Networking { case ServerPacketHeader.PING_REQUEST: IWriteMessage response = new WriteOnlyMessage(); - response.Write((byte)ClientPacketHeader.PING_RESPONSE); + response.WriteByte((byte)ClientPacketHeader.PING_RESPONSE); byte requestLen = inc.ReadByte(); - response.Write(requestLen); + response.WriteByte(requestLen); for (int i = 0; i < requestLen; i++) { byte b = inc.ReadByte(); - response.Write(b); + response.WriteByte(b); } - clientPeer.Send(response, DeliveryMethod.Unreliable); + ClientPeer.Send(response, DeliveryMethod.Unreliable); break; case ServerPacketHeader.CLIENT_PINGS: byte clientCount = inc.ReadByte(); @@ -771,7 +615,7 @@ namespace Barotrauma.Networking { byte clientId = inc.ReadByte(); UInt16 clientPing = inc.ReadUInt16(); - Client client = ConnectedClients.Find(c => c.ID == clientId); + Client client = ConnectedClients.Find(c => c.SessionId == clientId); if (client != null) { client.Ping = clientPing; @@ -833,7 +677,7 @@ namespace Barotrauma.Networking } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); - readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); + readyToStartMsg.WriteByte((byte)ClientPacketHeader.RESPONSE_STARTGAME); if (campaign != null) { campaign.PendingSubmarineSwitch = null; } GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; @@ -851,13 +695,13 @@ namespace Barotrauma.Networking campaign.LastSaveID == campaignSaveID && campaignUpdateIDs.All(kvp => campaign.GetLastUpdateIdForFlag(kvp.Key) == kvp.Value); } - readyToStartMsg.Write(readyToStart); + readyToStartMsg.WriteBoolean(readyToStart); DebugConsole.Log(readyToStart ? "Ready to start." : "Not ready to start."); WriteCharacterInfo(readyToStartMsg); - clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); + ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { @@ -884,7 +728,7 @@ namespace Barotrauma.Networking //waiting for a save file if (campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) && - fileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) + FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) { return; } @@ -979,7 +823,7 @@ namespace Barotrauma.Networking ReadyCheck.ClientRead(inc); break; case ServerPacketHeader.FILE_TRANSFER: - fileReceiver.ReadMessage(inc); + FileReceiver.ReadMessage(inc); break; case ServerPacketHeader.TRAITOR_MESSAGE: ReadTraitorMessage(inc); @@ -1081,207 +925,149 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Started; } - - private void OnDisconnect(bool disableReconnect) + /// + /// Fires when the ClientPeer gets disconnected from the server. Does not necessarily mean the client is shutting down, we may still be able to reconnect. + /// + private void OnClientPeerDisconnect(PeerDisconnectPacket disconnectPacket) { + bool wasConnected = connected; + connected = false; + connectCancelled = true; + CoroutineManager.StopCoroutines("WaitForStartingInfo"); - reconnectBox?.Close(); - reconnectBox = null; - - GameMain.ModDownloadScreen.Reset(); - ContentPackageManager.EnabledPackages.Restore(); + CloseReconnectBox(); GUI.ClearCursorWait(); - if (disableReconnect) { allowReconnect = false; } - if (!this.allowReconnect) { CancelConnect(); } - + ChildServerRelay.ShutDown(); + if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); } - } - private void HandleDisconnectMessage(string disconnectMsg) - { - disconnectMsg = disconnectMsg ?? ""; - - string[] splitMsg = disconnectMsg.Split('/'); - DisconnectReason disconnectReason = DisconnectReason.Unknown; - bool disconnectReasonIncluded = false; - if (splitMsg.Length > 0) - { - if (Enum.TryParse(splitMsg[0], out disconnectReason)) { disconnectReasonIncluded = true; } - } - - if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage || - disconnectReason == DisconnectReason.Banned || - disconnectReason == DisconnectReason.Kicked || - disconnectReason == DisconnectReason.TooManyFailedLogins) - { - allowReconnect = false; - } - - DebugConsole.NewMessage("Received a disconnect message (" + disconnectMsg + ")"); - - if (disconnectReason != DisconnectReason.Banned && - disconnectReason != DisconnectReason.ServerShutdown && - disconnectReason != DisconnectReason.TooManyFailedLogins && - disconnectReason != DisconnectReason.NotOnWhitelist && - disconnectReason != DisconnectReason.MissingContentPackage && - disconnectReason != DisconnectReason.InvalidVersion) + if (disconnectPacket.ShouldCreateAnalyticsEvent) { GameAnalyticsManager.AddErrorEventOnce( "GameClient.HandleDisconnectMessage", GameAnalyticsManager.ErrorSeverity.Debug, - "Client received a disconnect message. Reason: " + disconnectReason.ToString()); + $"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}"); } - - if (disconnectReason == DisconnectReason.ServerFull) + + if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull) { - CoroutineManager.StopCoroutines("WaitForStartingInfo"); - //already waiting for a slot to free up, stop waiting for starting info and - //let WaitInServerQueue reattempt connecting later - if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) - { - return; - } - - reconnectBox?.Close(); reconnectBox = null; - - var queueBox = new GUIMessageBox( - TextManager.Get("DisconnectReason.ServerFull"), - TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); - - queueBox.Buttons[0].OnClicked += queueBox.Close; - queueBox.Buttons[1].OnClicked += queueBox.Close; - queueBox.Buttons[1].OnClicked += (btn, userdata) => - { - reconnectBox?.Close(); reconnectBox = null; - CoroutineManager.StartCoroutine(WaitInServerQueue(), "WaitInServerQueue"); - return true; - }; - return; + AskToWaitInQueue(); } - else + else if (disconnectPacket.ShouldAttemptReconnect && !IsServerOwner && wasConnected) { - //disconnected/denied for some other reason than the server being full - // -> stop queuing and show a message box - waitInServerQueueBox?.Close(); - waitInServerQueueBox = null; - CoroutineManager.StopCoroutines("WaitInServerQueue"); - } - - bool eventSyncError = - disconnectReason == DisconnectReason.ExcessiveDesyncOldEvent || - disconnectReason == DisconnectReason.ExcessiveDesyncRemovedEvent || - disconnectReason == DisconnectReason.SyncTimeout; - - if (allowReconnect && - (disconnectReason == DisconnectReason.Unknown || eventSyncError)) - { - if (eventSyncError) + if (disconnectPacket.IsEventSyncError) { GameMain.NetLobbyScreen.Select(); GameMain.GameSession?.EndRound("", null); - gameStarted = false; + GameStarted = false; myCharacter = null; } - - DebugConsole.NewMessage("Attempting to reconnect..."); - - //if the first part of the message is the disconnect reason Enum, don't include it in the popup message - LocalizedString msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); - msg = msg.IsNullOrWhiteSpace() ? - TextManager.Get("ConnectionLostReconnecting") : - msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); - - reconnectBox?.Close(); - reconnectBox = new GUIMessageBox( - TextManager.Get("ConnectionLost"), msg, - new LocalizedString[] { TextManager.Get("Cancel") }); - reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; - connected = false; - - var prevContentPackages = clientPeer.ServerContentPackages; - ConnectToServer(serverEndpoint, serverName); - if (clientPeer != null) - { - //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match - clientPeer.ServerContentPackages = prevContentPackages; - } + AttemptReconnect(disconnectPacket); } else { - connected = false; - connectCancelled = true; - - LocalizedString msg = ""; - if (disconnectReason == DisconnectReason.Unknown) + ReturnToPreviousMenu(null, null); + new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) { - DebugConsole.NewMessage("Not attempting to reconnect (unknown disconnect reason)."); - msg = disconnectMsg; - } - else - { - DebugConsole.NewMessage("Not attempting to reconnect (DisconnectReason doesn't allow reconnection)."); - msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()) + " "; - - for (int i = 1; i < splitMsg.Length; i++) - { - msg += TextManager.GetServerMessage(splitMsg[i]); - } - - if (disconnectReason == DisconnectReason.ServerCrashed && IsServerOwner) - { - msg = TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", ChildServerRelay.CrashReportFilePath); - } - } - - reconnectBox?.Close(); - - if (msg == Lidgren.Network.NetConnection.NoResponseMessage) - { - //display a generic "could not connect" popup if the message is Lidgren's "failed to establish connection" - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer")); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } - else - { - var msgBox = new GUIMessageBox(TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer"), msg); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } - - if (disconnectReason == DisconnectReason.InvalidName) - { - GameMain.ServerListScreen.ClientNameBox.Text = ""; - GameMain.ServerListScreen.ClientNameBox.Flash(flashDuration: 5.0f); - GameMain.ServerListScreen.ClientNameBox.Select(); - } + DisplayInLoadingScreens = true + }; } } + private void CreateReconnectBox(LocalizedString headerText, LocalizedString bodyText) + { + reconnectBox = new GUIMessageBox( + headerText, + bodyText, + new LocalizedString[] { TextManager.Get("Cancel") }) + { + DisplayInLoadingScreens = true + }; + reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; + reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; + } + + private void CloseReconnectBox() + { + reconnectBox?.Close(); + reconnectBox = null; + } + + private void AskToWaitInQueue() + { + CoroutineManager.StopCoroutines("WaitForStartingInfo"); + //already waiting for a slot to free up, stop waiting for starting info and + //let WaitInServerQueue reattempt connecting later + if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) + { + return; + } + + var queueBox = new GUIMessageBox( + TextManager.Get("DisconnectReason.ServerFull"), + TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); + + queueBox.Buttons[0].OnClicked += queueBox.Close; + queueBox.Buttons[1].OnClicked += queueBox.Close; + queueBox.Buttons[1].OnClicked += (btn, userdata) => + { + CloseReconnectBox(); + CoroutineManager.StartCoroutine(WaitInServerQueue(), "WaitInServerQueue"); + return true; + }; + } + + private void AttemptReconnect(PeerDisconnectPacket peerDisconnectPacket) + { + connectCancelled = false; + + CreateReconnectBox( + TextManager.Get("ConnectionLost"), + peerDisconnectPacket.ReconnectMessage); + + var prevContentPackages = ClientPeer.ServerContentPackages; + //decrement lobby update ID to make sure we update the lobby when we reconnect + GameMain.NetLobbyScreen.LastUpdateID--; + InitiateServerJoin(ServerName); + if (ClientPeer != null) + { + //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match + ClientPeer.ContentPackageOrderReceived = true; + ClientPeer.ServerContentPackages = prevContentPackages; + } + } + private void OnConnectionInitializationComplete() { if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); - Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); + Steamworks.SteamFriends.SetRichPresence("servername", ServerName); + #warning TODO: use Steamworks localization functionality + Steamworks.SteamFriends.SetRichPresence("status", + TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName).Value); + Steamworks.SteamFriends.SetRichPresence("connect", + $"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {serverEndpoint}"); } canStart = true; connected = true; - VoipClient = new VoipClient(this, clientPeer); + VoipClient = new VoipClient(this, ClientPeer); - if (Screen.Selected != GameMain.GameScreen) + //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods + if (!(Screen.Selected is GameScreen) && !(Screen.Selected is RoundSummaryScreen) && !(Screen.Selected is NetLobbyScreen)) { GameMain.ModDownloadScreen.Select(); } else { - entityEventManager.ClearSelf(); + EntityEventManager.ClearSelf(); foreach (Character c in Character.CharacterList) { c.ResetNetState(); @@ -1312,7 +1098,7 @@ namespace Barotrauma.Networking { if (!CoroutineManager.IsCoroutineRunning("WaitForStartingInfo")) { - ConnectToServer(serverEndpoint, serverName); + InitiateServerJoin(ServerName); yield return new WaitForSeconds(5.0f); } yield return new WaitForSeconds(0.5f); @@ -1383,15 +1169,15 @@ namespace Barotrauma.Networking private void ReadPermissions(IReadMessage inc) { List permittedConsoleCommands = new List(); - byte clientID = inc.ReadByte(); + byte clientId = inc.ReadByte(); ClientPermissions permissions = ClientPermissions.None; List permittedCommands = new List(); Client.ReadPermissions(inc, out permissions, out permittedCommands); - Client targetClient = ConnectedClients.Find(c => c.ID == clientID); + Client targetClient = ConnectedClients.Find(c => c.SessionId == clientId); targetClient?.SetPermissions(permissions, permittedCommands); - if (clientID == myID) + if (clientId == SessionId) { SetMyPermissions(permissions, permittedCommands.Select(command => command.names[0])); } @@ -1505,7 +1291,7 @@ namespace Barotrauma.Networking //(for example, due to a missing sub file or an error) GameMain.NetLobbyScreen.ShowSpectateButton(); - entityEventManager.Clear(); + EntityEventManager.Clear(); LastSentEntityEventID = 0; EndVoteTickBox.Selected = false; @@ -1526,19 +1312,19 @@ namespace Barotrauma.Networking } bool respawnAllowed = inc.ReadBoolean(); - serverSettings.AllowDisguises = inc.ReadBoolean(); - serverSettings.AllowRewiring = inc.ReadBoolean(); - serverSettings.AllowFriendlyFire = inc.ReadBoolean(); - serverSettings.LockAllDefaultWires = inc.ReadBoolean(); - serverSettings.AllowRagdollButton = inc.ReadBoolean(); - serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); - serverSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); + ServerSettings.AllowDisguises = inc.ReadBoolean(); + ServerSettings.AllowRewiring = inc.ReadBoolean(); + ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); + ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); + ServerSettings.AllowRagdollButton = inc.ReadBoolean(); + ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); + ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; - serverSettings.ReadMonsterEnabled(inc); + ServerSettings.ReadMonsterEnabled(inc); Rand.SetSyncedSeed(seed); @@ -1594,7 +1380,7 @@ namespace Barotrauma.Networking errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation + " != " + subHash; } } - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -1605,7 +1391,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName || GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash) { - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ")."; DebugConsole.ThrowError(errorMsg); @@ -1637,7 +1423,7 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (campaign ID does not match)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1645,7 +1431,7 @@ namespace Barotrauma.Networking } else if (campaign.Map == null) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1660,7 +1446,7 @@ namespace Barotrauma.Networking { if (DateTime.Now > saveFileTimeOut) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1698,7 +1484,7 @@ namespace Barotrauma.Networking } } - if (clientPeer == null) + if (ClientPeer == null) { DebugConsole.ThrowError("There was an error initializing the round (disconnected during the StartGame coroutine.)"); roundInitStatus = RoundInitStatus.Error; @@ -1712,8 +1498,8 @@ namespace Barotrauma.Networking DateTime requestFinalizeTime = DateTime.Now; TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); - clientPeer.Send(msg, DeliveryMethod.Unreliable); + msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); + ClientPeer.Send(msg, DeliveryMethod.Unreliable); GUIMessageBox interruptPrompt = null; @@ -1726,8 +1512,8 @@ namespace Barotrauma.Networking if (DateTime.Now > requestFinalizeTime) { msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); - clientPeer.Send(msg, DeliveryMethod.Unreliable); + msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); + ClientPeer.Send(msg, DeliveryMethod.Unreliable); requestFinalizeTime = DateTime.Now + requestFinalizeInterval; } if (DateTime.Now > timeOut && interruptPrompt == null) @@ -1741,7 +1527,7 @@ namespace Barotrauma.Networking { roundInitStatus = RoundInitStatus.Interrupted; DebugConsole.ThrowError("Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Returning to the lobby..."); - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); interruptPrompt.Close(); interruptPrompt = null; @@ -1834,10 +1620,10 @@ namespace Barotrauma.Networking if (respawnAllowed) { - respawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); + RespawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); } - gameStarted = true; + GameStarted = true; ServerSettings.ServerDetailsChanged = true; if (roundSummary != null) @@ -1869,7 +1655,7 @@ namespace Barotrauma.Networking yield return new WaitForSeconds(1.0f); } - if (!gameStarted) + if (!GameStarted) { GameMain.NetLobbyScreen.Select(); yield return CoroutineStatus.Success; @@ -1879,26 +1665,34 @@ namespace Barotrauma.Networking ServerSettings.ServerDetailsChanged = true; - gameStarted = false; + GameStarted = false; Character.Controlled = null; WaitForNextRoundRespawn = null; SpawnAsTraitor = false; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - respawnManager = null; + RespawnManager = null; if (Screen.Selected == GameMain.GameScreen) { + Submarine refSub = Submarine.MainSub; + if (Submarine.MainSubs[1] != null && + GameMain.GameSession.GameMode is PvPMode && + GameMain.GameSession.WinningTeam.HasValue && GameMain.GameSession.WinningTeam == CharacterTeamType.Team1) + { + refSub = Submarine.MainSubs[1]; + } + // Enable characters near the main sub for the endCinematic foreach (Character c in Character.CharacterList) { - if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) + if (Vector2.DistanceSquared(refSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) { c.Enabled = true; } } - EndCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight); + EndCinematic = new CameraTransition(refSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight); while (EndCinematic.Running && Screen.Selected == GameMain.GameScreen) { yield return CoroutineStatus.Running; @@ -1923,7 +1717,7 @@ namespace Barotrauma.Networking private void ReadInitialUpdate(IReadMessage inc) { - myID = inc.ReadByte(); + SessionId = inc.ReadByte(); UInt16 subListCount = inc.ReadUInt16(); ServerSubmarines.Clear(); @@ -1932,6 +1726,7 @@ namespace Barotrauma.Networking string subName = inc.ReadString(); string subHash = inc.ReadString(); byte subClass = inc.ReadByte(); + bool isShuttle = inc.ReadBoolean(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); @@ -1941,20 +1736,21 @@ namespace Barotrauma.Networking { SubmarineClass = (SubmarineClass)subClass }; + if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); } } matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; ServerSubmarines.Add(matchingSub); } GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, ServerSubmarines); - GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, ServerSubmarines); + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, ServerSubmarines.Where(s => s.HasTag(SubmarineTag.Shuttle))); - gameStarted = inc.ReadBoolean(); + GameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); ReadPermissions(inc); - if (gameStarted) + if (GameStarted) { if (Screen.Selected != GameMain.GameScreen) { @@ -1983,22 +1779,21 @@ namespace Barotrauma.Networking foreach (TempClient tc in tempClients) { //see if the client already exists - var existingClient = ConnectedClients.Find(c => c.ID == tc.ID && c.Name == tc.Name); + var existingClient = ConnectedClients.Find(c => c.SessionId == tc.SessionId && c.Name == tc.Name); if (existingClient == null) //if not, create it { - existingClient = new Client(tc.Name, tc.ID) + existingClient = new Client(tc.Name, tc.SessionId) { - SteamID = tc.SteamID, + AccountInfo = tc.AccountInfo, Muted = tc.Muted, InGame = tc.InGame, - AllowKicking = tc.AllowKicking, IsOwner = tc.IsOwner }; - ConnectedClients.Add(existingClient); + otherClients.Add(existingClient); refreshCampaignUI = true; GameMain.NetLobbyScreen.AddPlayer(existingClient); } - existingClient.NameID = tc.NameID; + existingClient.NameId = tc.NameId; existingClient.PreferredJob = tc.PreferredJob; existingClient.PreferredTeam = tc.PreferredTeam; existingClient.Character = null; @@ -2006,25 +1801,24 @@ namespace Barotrauma.Networking existingClient.Muted = tc.Muted; existingClient.InGame = tc.InGame; existingClient.IsOwner = tc.IsOwner; - existingClient.AllowKicking = tc.AllowKicking; existingClient.IsDownloading = tc.IsDownloading; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); - if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterID > 0) + if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterId > 0) { - existingClient.CharacterID = tc.CharacterID; + existingClient.CharacterID = tc.CharacterId; } - if (existingClient.ID == myID) + if (existingClient.SessionId == SessionId) { existingClient.SetPermissions(permissions, permittedConsoleCommands); - if (!NetIdUtils.IdMoreRecent(nameId, tc.NameID)) + if (!NetIdUtils.IdMoreRecent(nameId, tc.NameId)) { - name = tc.Name; - nameId = tc.NameID; + Name = tc.Name; + nameId = tc.NameId; } if (GameMain.NetLobbyScreen.CharacterNameBox != null && !GameMain.NetLobbyScreen.CharacterNameBox.Selected) { - GameMain.NetLobbyScreen.CharacterNameBox.Text = name; + GameMain.NetLobbyScreen.CharacterNameBox.Text = Name; } } currentClients.Add(existingClient); @@ -2035,14 +1829,14 @@ namespace Barotrauma.Networking if (!currentClients.Contains(ConnectedClients[i])) { GameMain.NetLobbyScreen.RemovePlayer(ConnectedClients[i]); - ConnectedClients[i].Dispose(); - ConnectedClients.RemoveAt(i); + otherClients[i].Dispose(); + otherClients.RemoveAt(i); refreshCampaignUI = true; } } foreach (Client client in ConnectedClients) { - int index = previouslyConnectedClients.FindIndex(c => c.ID == client.ID); + int index = previouslyConnectedClients.FindIndex(c => c.SessionId == client.SessionId); if (index < 0) { if (previouslyConnectedClients.Count > 100) @@ -2058,15 +1852,15 @@ namespace Barotrauma.Networking } if (updateClientListId) { LastClientListUpdateID = listId; } - if (clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2POwnerPeer) { TaskPool.Add("WaitForPingDataAsync (owner)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => { - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); }); - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); } } @@ -2146,11 +1940,11 @@ namespace Barotrauma.Networking (isInitialUpdate || initialUpdateReceived)) { ReadWriteMessage settingsBuf = new ReadWriteMessage(); - settingsBuf.Write(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0; - serverSettings.ClientRead(settingsBuf); + settingsBuf.WriteBytes(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0; + ServerSettings.ClientRead(settingsBuf); if (!IsServerOwner) { - ServerInfo info = serverSettings.GetServerListInfo(); + ServerInfo info = CreateServerInfoFromSettings(); GameMain.ServerListScreen.AddToRecentServers(info); GameMain.NetLobbyScreen.Favorite.Visible = true; GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info); @@ -2162,10 +1956,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.LastUpdateID = updateID; - serverSettings.ServerLog.ServerName = serverSettings.ServerName; + ServerSettings.ServerLog.ServerName = ServerSettings.ServerName; - if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = serverSettings.ServerName; } - if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = serverSettings.ServerMessageText; } + if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = ServerSettings.ServerName; } + if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = ServerSettings.ServerMessageText; } GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } @@ -2195,13 +1989,13 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(botCount); GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); - serverSettings.VoiceChatEnabled = voiceChatEnabled; - serverSettings.AllowSubVoting = allowSubVoting; - serverSettings.AllowModeVoting = allowModeVoting; + ServerSettings.VoiceChatEnabled = voiceChatEnabled; + ServerSettings.AllowSubVoting = allowSubVoting; + ServerSettings.AllowModeVoting = allowModeVoting; - if (clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2POwnerPeer) { - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); } GUI.KeyboardDispatcher.Subscriber = prevDispatcher; @@ -2316,7 +2110,7 @@ namespace Barotrauma.Networking break; case ServerNetObject.ENTITY_EVENT: case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) + if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) { return; } @@ -2398,38 +2192,38 @@ namespace Barotrauma.Networking private void SendLobbyUpdate() { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); + outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); - outmsg.Write((byte)ClientNetObject.SYNC_IDS); - outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.Write(ChatMessage.LastID); - outmsg.Write(LastClientListUpdateID); - outmsg.Write(nameId); - outmsg.Write(name); + outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(LastClientListUpdateID); + outmsg.WriteUInt16(nameId); + outmsg.WriteString(Name); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; if (jobPreferences.Count > 0) { - outmsg.Write(jobPreferences[0].Prefab.Identifier); + outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier); } else { - outmsg.Write(""); + outmsg.WriteIdentifier(Identifier.Empty); } - outmsg.Write((byte)MultiplayerPreferences.Instance.TeamPreference); + outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { - outmsg.Write((UInt16)0); + outmsg.WriteUInt16((UInt16)0); } else { - outmsg.Write(campaign.LastSaveID); - outmsg.Write(campaign.CampaignID); + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) { - outmsg.Write(campaign.GetLastUpdateIdForFlag(netFlag)); + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag)); } - outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); @@ -2442,48 +2236,48 @@ namespace Barotrauma.Networking } chatMsgQueue[i].ClientWrite(outmsg); } - outmsg.Write((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } - clientPeer.Send(outmsg, DeliveryMethod.Unreliable); + ClientPeer.Send(outmsg, DeliveryMethod.Unreliable); } private void SendIngameUpdate() { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ClientPacketHeader.UPDATE_INGAME); - outmsg.Write(entityEventManager.MidRoundSyncingDone); + outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_INGAME); + outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone); outmsg.WritePadBits(); - outmsg.Write((byte)ClientNetObject.SYNC_IDS); + outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.Write(ChatMessage.LastID); - outmsg.Write(entityEventManager.LastReceivedID); - outmsg.Write(LastClientListUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(EntityEventManager.LastReceivedID); + outmsg.WriteUInt16(LastClientListUpdateID); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { - outmsg.Write((UInt16)0); + outmsg.WriteUInt16((UInt16)0); } else { - outmsg.Write(campaign.LastSaveID); - outmsg.Write(campaign.CampaignID); + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) { - outmsg.Write(campaign.GetLastUpdateIdForFlag(flag)); + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); } - outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } Character.Controlled?.ClientWriteInput(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); - entityEventManager.Write(outmsg, clientPeer?.ServerConnection); + EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection); chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2496,19 +2290,19 @@ namespace Barotrauma.Networking chatMsgQueue[i].ClientWrite(outmsg); } - outmsg.Write((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } - clientPeer.Send(outmsg, DeliveryMethod.Unreliable); + ClientPeer.Send(outmsg, DeliveryMethod.Unreliable); } public void SendChatMessage(ChatMessage msg) { - if (clientPeer?.ServerConnection == null) { return; } + if (ClientPeer?.ServerConnection == null) { return; } lastQueueChatMsgID++; msg.NetStateID = lastQueueChatMsgID; chatMsgQueue.Add(msg); @@ -2516,13 +2310,13 @@ namespace Barotrauma.Networking public void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Default) { - if (clientPeer?.ServerConnection == null) { return; } + if (ClientPeer?.ServerConnection == null) { return; } ChatMessage chatMessage = ChatMessage.Create( - gameStarted && myCharacter != null ? myCharacter.Name : name, + GameStarted && myCharacter != null ? myCharacter.Name : Name, message, type, - gameStarted && myCharacter != null ? myCharacter : null); + GameStarted && myCharacter != null ? myCharacter : null); chatMessage.ChatMode = GameMain.ActiveChatMode; lastQueueChatMsgID++; @@ -2535,9 +2329,9 @@ namespace Barotrauma.Networking { WaitForNextRoundRespawn = waitForNextRoundRespawn; IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.READY_TO_SPAWN); - msg.Write((bool)waitForNextRoundRespawn); - clientPeer?.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN); + msg.WriteBoolean((bool)waitForNextRoundRespawn); + ClientPeer?.Send(msg, DeliveryMethod.Reliable); } public void RequestFile(FileTransferType fileType, string file, string fileHash) @@ -2548,15 +2342,15 @@ namespace Barotrauma.Networking $"Sending a file request to the server (type: {fileType}, path: {file ?? "null"}"); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.FILE_REQUEST); - msg.Write((byte)FileTransferMessageType.Initiate); - msg.Write((byte)fileType); + msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST); + msg.WriteByte((byte)FileTransferMessageType.Initiate); + msg.WriteByte((byte)fileType); if (fileType != FileTransferType.CampaignSave) { - msg.Write(file ?? throw new ArgumentNullException(nameof(file))); - msg.Write(fileHash ?? throw new ArgumentNullException(nameof(fileHash))); + msg.WriteString(file ?? throw new ArgumentNullException(nameof(file))); + msg.WriteString(fileHash ?? throw new ArgumentNullException(nameof(fileHash))); } - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void CancelFileTransfer(FileReceiver.FileTransferIn transfer) @@ -2573,21 +2367,21 @@ namespace Barotrauma.Networking transfer.RecordOffsetAckTime(); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.FILE_REQUEST); - msg.Write((byte)FileTransferMessageType.Data); - msg.Write((byte)transfer.ID); - msg.Write(expecting); - msg.Write(lastSeen); - clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); + msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST); + msg.WriteByte((byte)FileTransferMessageType.Data); + msg.WriteByte((byte)transfer.ID); + msg.WriteInt32(expecting); + msg.WriteInt32(lastSeen); + ClientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); } public void CancelFileTransfer(int id) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.FILE_REQUEST); - msg.Write((byte)FileTransferMessageType.Cancel); - msg.Write((byte)id); - clientPeer.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST); + msg.WriteByte((byte)FileTransferMessageType.Cancel); + msg.WriteByte((byte)id); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } private void OnFileReceived(FileReceiver.FileTransferIn transfer) @@ -2615,13 +2409,12 @@ namespace Barotrauma.Networking var subElement = subListChildren.FirstOrDefault(c => ((SubmarineInfo)c.UserData).Name == newSub.Name && ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation); - if (subElement == null) continue; + if (subElement == null) { continue; } Color newSubTextColor = new Color(subElement.GetChild().TextColor, 1.0f); subElement.GetChild().TextColor = newSubTextColor; - GUITextBlock classTextBlock = subElement.GetChildByUserData("classtext") as GUITextBlock; - if (classTextBlock != null) + if (subElement.GetChildByUserData("classtext") is GUITextBlock classTextBlock) { Color newSubClassTextColor = new Color(classTextBlock.TextColor, 0.8f); classTextBlock.Text = TextManager.Get($"submarineclass.{newSub.SubmarineClass}"); @@ -2641,7 +2434,7 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue && GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.MD5Hash.StringRepresentation) + GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.StringRepresentation) { GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox); } @@ -2734,7 +2527,7 @@ namespace Barotrauma.Networking { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } - entityEventManager.CreateEvent(clientSerializable, extraData); + EntityEventManager.CreateEvent(clientSerializable, extraData); } public bool HasPermission(ClientPermissions permission) @@ -2761,33 +2554,27 @@ namespace Barotrauma.Networking return false; } - public override void Disconnect() + public void Quit() { GameMain.LuaCs.Stop(); - - allowReconnect = false; - - if (clientPeer is SteamP2PClientPeer || clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2PClientPeer || ClientPeer is SteamP2POwnerPeer) { SteamManager.LeaveLobby(); } + GameMain.ModDownloadScreen.Reset(); + ContentPackageManager.EnabledPackages.Restore(); + CampaignMode.StartRoundCancellationToken?.Cancel(); - clientPeer?.Close(); - clientPeer = null; + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + ClientPeer = null; - List activeTransfers = new List(FileReceiver.ActiveTransfers); - foreach (var fileTransfer in activeTransfers) + foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) { FileReceiver.StopTransfer(fileTransfer, deleteFile: true); } - if (HasPermission(ClientPermissions.ServerLog)) - { - serverSettings.ServerLog?.Save(); - } - if (ChildServerRelay.Process != null) { int checks = 0; @@ -2802,6 +2589,7 @@ namespace Barotrauma.Networking } } ChildServerRelay.ShutDown(); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); characterInfo?.Remove(); @@ -2814,59 +2602,58 @@ namespace Barotrauma.Networking public void SendCharacterInfo(string newName = null) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); + msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); WriteCharacterInfo(msg, newName); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer?.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + ClientPeer?.Send(msg, DeliveryMethod.Reliable); } public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { - msg.Write(characterInfo == null); + msg.WriteBoolean(characterInfo == null); if (characterInfo == null) { return; } - msg.Write(newName ?? string.Empty); + msg.WriteString(newName ?? string.Empty); - msg.Write((byte)characterInfo.Head.Preset.TagSet.Count); + msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count); foreach (Identifier tag in characterInfo.Head.Preset.TagSet) { - msg.Write(tag); + msg.WriteIdentifier(tag); } - msg.Write((byte)characterInfo.Head.HairIndex); - msg.Write((byte)characterInfo.Head.BeardIndex); - msg.Write((byte)characterInfo.Head.MoustacheIndex); - msg.Write((byte)characterInfo.Head.FaceAttachmentIndex); + msg.WriteByte((byte)characterInfo.Head.HairIndex); + msg.WriteByte((byte)characterInfo.Head.BeardIndex); + msg.WriteByte((byte)characterInfo.Head.MoustacheIndex); + msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex); msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); msg.WriteColorR8G8B8(characterInfo.Head.HairColor); msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; int count = Math.Min(jobPreferences.Count, 3); - msg.Write((byte)count); + msg.WriteByte((byte)count); for (int i = 0; i < count; i++) { - msg.Write(jobPreferences[i].Prefab.Identifier); - msg.Write((byte)jobPreferences[i].Variant); + msg.WriteIdentifier(jobPreferences[i].Prefab.Identifier); + msg.WriteByte((byte)jobPreferences[i].Variant); } } public void Vote(VoteType voteType, object data) { - if (clientPeer == null) { return; } + if (ClientPeer == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); - msg.Write((byte)ClientNetObject.VOTE); + msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); + msg.WriteByte((byte)ClientNetObject.VOTE); Voting.ClientWrite(msg, voteType, data); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void VoteForKick(Client votedClient) { if (votedClient == null) { return; } - votedClient.AddKickVote(ConnectedClients.FirstOrDefault(c => c.ID == myID)); Vote(VoteType.Kick, votedClient); } @@ -2915,44 +2702,53 @@ namespace Barotrauma.Networking public override void KickPlayer(string kickedName, string reason) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.Kick); - msg.Write(kickedName); - msg.Write(reason); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.Kick); + msg.WriteString(kickedName); + msg.WriteString(reason); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public override void BanPlayer(string kickedName, string reason, bool range = false, TimeSpan? duration = null) + public override void BanPlayer(string kickedName, string reason, TimeSpan? duration = null) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.Ban); - msg.Write(kickedName); - msg.Write(reason); - msg.Write(range); - msg.Write(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.Ban); + msg.WriteString(kickedName); + msg.WriteString(reason); + msg.WriteDouble(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public override void UnbanPlayer(string playerName, string playerIP) + public override void UnbanPlayer(string playerName) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.Unban); - msg.Write(string.IsNullOrEmpty(playerName) ? "" : playerName); - msg.Write(string.IsNullOrEmpty(playerIP) ? "" : playerIP); - clientPeer.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.Unban); + msg.WriteBoolean(true); msg.WritePadBits(); + msg.WriteString(playerName); + ClientPeer.Send(msg, DeliveryMethod.Reliable); + } + + public override void UnbanPlayer(Endpoint endpoint) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.Unban); + msg.WriteBoolean(false); msg.WritePadBits(); + msg.WriteString(endpoint.StringRepresentation); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void UpdateClientPermissions(Client targetClient) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.ManagePermissions); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.ManagePermissions); targetClient.WritePermissions(msg); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendCampaignState() @@ -2963,11 +2759,11 @@ namespace Barotrauma.Networking return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.ManageCampaign); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendConsoleCommand(string command) @@ -2979,14 +2775,14 @@ namespace Barotrauma.Networking } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.ConsoleCommands); - msg.Write(command); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.ConsoleCommands); + msg.WriteString(command); Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - msg.Write(cursorWorldPos.X); - msg.Write(cursorWorldPos.Y); + msg.WriteSingle(cursorWorldPos.X); + msg.WriteSingle(cursorWorldPos.Y); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2995,37 +2791,28 @@ namespace Barotrauma.Networking public void RequestStartRound(bool continueCampaign = false) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.ManageRound); - msg.Write(false); //indicates round start - msg.Write(continueCampaign); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); + msg.WriteBoolean(false); //indicates round start + msg.WriteBoolean(continueCampaign); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// /// Tell the server to select a submarine (permission required) /// - public void RequestSelectSub(int subIndex, bool isShuttle) + public void RequestSelectSub(SubmarineInfo sub, bool isShuttle) { - if (!HasPermission(ClientPermissions.SelectSub)) return; - - var subList = isShuttle ? GameMain.NetLobbyScreen.ShuttleList.ListBox : GameMain.NetLobbyScreen.SubList; - - if (subIndex < 0 || subIndex >= subList.Content.CountChildren) - { - DebugConsole.ThrowError("Submarine index out of bounds (" + subIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } + if (!HasPermission(ClientPermissions.SelectSub) || sub == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.SelectSub); - msg.Write(isShuttle); msg.WritePadBits(); - msg.Write((UInt16)subIndex); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - - clientPeer.Send(msg, DeliveryMethod.Reliable); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.SelectSub); + msg.WriteBoolean(isShuttle); msg.WritePadBits(); + msg.WriteString(sub.MD5Hash.StringRepresentation); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -3041,12 +2828,12 @@ namespace Barotrauma.Networking } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.SelectMode); - msg.Write((UInt16)modeIndex); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.SelectMode); + msg.WriteUInt16((UInt16)modeIndex); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings) @@ -3057,32 +2844,32 @@ namespace Barotrauma.Networking saveName = Path.GetFileNameWithoutExtension(saveName); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); - msg.Write(true); msg.WritePadBits(); - msg.Write(saveName); - msg.Write(mapSeed); - msg.Write(sub.Name); - msg.Write(sub.MD5Hash.StringRepresentation); - msg.Write(settings); + msg.WriteBoolean(true); msg.WritePadBits(); + msg.WriteString(saveName); + msg.WriteString(mapSeed); + msg.WriteString(sub.Name); + msg.WriteString(sub.MD5Hash.StringRepresentation); + msg.WriteNetSerializableStruct(settings); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SetupLoadCampaign(string saveName) { - if (clientPeer == null) { return; } + if (ClientPeer == null) { return; } GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; GameMain.NetLobbyScreen.CampaignFrame.Visible = false; IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); - msg.Write(false); msg.WritePadBits(); - msg.Write(saveName); + msg.WriteBoolean(false); msg.WritePadBits(); + msg.WriteString(saveName); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -3091,12 +2878,12 @@ namespace Barotrauma.Networking public void RequestRoundEnd(bool save) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.ManageRound); - msg.Write(true); //indicates round end - msg.Write(save); + msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); + msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); + msg.WriteBoolean(true); //indicates round end + msg.WriteBoolean(save); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public bool SpectateClicked(GUIButton button, object userData) @@ -3112,25 +2899,25 @@ namespace Barotrauma.Networking if (button != null) { button.Enabled = false; } if (campaign != null) { LateCampaignJoin = true; } - if (clientPeer == null) { return false; } + if (ClientPeer == null) { return false; } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); - readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); + readyToStartMsg.WriteByte((byte)ClientPacketHeader.RESPONSE_STARTGAME); //assume we have the required sub files to start the round //(if not, we'll find out when the server sends the STARTGAME message and can initiate a file transfer) - readyToStartMsg.Write(true); + readyToStartMsg.WriteBoolean(true); WriteCharacterInfo(readyToStartMsg); - clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); + ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); return false; } public bool SetReadyToStart(GUITickBox tickBox) { - if (gameStarted) + if (GameStarted) { tickBox.Parent.Visible = false; return false; @@ -3141,9 +2928,9 @@ namespace Barotrauma.Networking public bool ToggleEndRoundVote(GUITickBox tickBox) { - if (!gameStarted) return false; + if (!GameStarted) return false; - if (!serverSettings.AllowEndVoting || !HasSpawned) + if (!ServerSettings.AllowEndVoting || !HasSpawned) { tickBox.Visible = false; return false; @@ -3244,19 +3031,19 @@ namespace Barotrauma.Networking return true; } - public virtual void AddToGUIUpdateList() + public void AddToGUIUpdateList() { if (GUI.DisableHUD || GUI.DisableUpperHUD) return; - if (gameStarted && + if (GameStarted && Screen.Selected == GameMain.GameScreen) { inGameHUD.AddToGUIUpdateList(); GameMain.NetLobbyScreen.FileTransferFrame?.AddToGUIUpdateList(); } - serverSettings.AddToGUIUpdateList(); - if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); + ServerSettings.AddToGUIUpdateList(); + if (ServerSettings.ServerLog.LogFrame != null) ServerSettings.ServerLog.LogFrame.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.PlayerFrame?.AddToGUIUpdateList(); } @@ -3276,10 +3063,10 @@ namespace Barotrauma.Networking UpdateLogButtonVisibility(); - if (gameStarted && Screen.Selected == GameMain.GameScreen) + if (GameStarted && Screen.Selected == GameMain.GameScreen) { - var controller = Character.Controlled?.SelectedConstruction?.GetComponent(); - bool disableButtons = Character.Controlled != null && (controller != null && controller.HideHUD); + bool disableButtons = Character.Controlled?.SelectedItem?.GetComponent() is Controller c1 && c1.HideHUD || + Character.Controlled?.SelectedSecondaryItem?.GetComponent() is Controller c2 && c2.HideHUD; buttonContainer.Visible = !disableButtons; if (!GUI.DisableHUD && !GUI.DisableUpperHUD) @@ -3333,13 +3120,13 @@ namespace Barotrauma.Networking } } - public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) + public void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (GUI.DisableHUD || GUI.DisableUpperHUD) return; - if (fileReceiver != null && fileReceiver.ActiveTransfers.Count > 0) + if (FileReceiver != null && FileReceiver.ActiveTransfers.Count > 0) { - var transfer = fileReceiver.ActiveTransfers.First(); + var transfer = FileReceiver.ActiveTransfers.First(); GameMain.NetLobbyScreen.FileTransferFrame.Visible = true; GameMain.NetLobbyScreen.FileTransferFrame.UserData = transfer; GameMain.NetLobbyScreen.FileTransferTitle.Text = @@ -3356,7 +3143,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.FileTransferFrame.Visible = false; } - if (!gameStarted || Screen.Selected != GameMain.GameScreen) { return; } + if (!GameStarted || Screen.Selected != GameMain.GameScreen) { return; } inGameHUD.DrawManually(spriteBatch); @@ -3382,7 +3169,7 @@ namespace Barotrauma.Networking EndVoteTickBox.Text = endRoundVoteText; } - if (respawnManager != null) + if (RespawnManager != null) { LocalizedString respawnText = string.Empty; Color textColor = Color.White; @@ -3391,26 +3178,26 @@ namespace Barotrauma.Networking Character.Controlled == null && Level.Loaded?.Type != LevelData.LevelType.Outpost && (characterInfo == null || HasSpawned); - if (respawnManager.CurrentState == RespawnManager.State.Waiting) + if (RespawnManager.CurrentState == RespawnManager.State.Waiting) { - if (respawnManager.RespawnCountdownStarted) + if (RespawnManager.RespawnCountdownStarted) { - float timeLeft = (float)(respawnManager.RespawnTime - DateTime.Now).TotalSeconds; + float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds; respawnText = TextManager.GetWithVariable( - respawnManager.UsingShuttle && !respawnManager.ForceSpawnInMainSub ? + RespawnManager.UsingShuttle && !RespawnManager.ForceSpawnInMainSub ? "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); } - else if (respawnManager.PendingRespawnCount > 0) + else if (RespawnManager.PendingRespawnCount > 0) { respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", - ("[deadplayers]", respawnManager.PendingRespawnCount.ToString()), - ("[requireddeadplayers]", respawnManager.RequiredRespawnCount.ToString())); + ("[deadplayers]", RespawnManager.PendingRespawnCount.ToString()), + ("[requireddeadplayers]", RespawnManager.RequiredRespawnCount.ToString())); } } - else if (respawnManager.CurrentState == RespawnManager.State.Transporting && - respawnManager.ReturnCountdownStarted) + else if (RespawnManager.CurrentState == RespawnManager.State.Transporting && + RespawnManager.ReturnCountdownStarted) { - float timeLeft = (float)(respawnManager.ReturnTime - DateTime.Now).TotalSeconds; + float timeLeft = (float)(RespawnManager.ReturnTime - DateTime.Now).TotalSeconds; respawnText = timeLeft <= 0.0f ? "" : TextManager.GetWithVariable("RespawnShuttleLeavingIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); @@ -3458,7 +3245,7 @@ namespace Barotrauma.Networking }*/ } - public virtual bool SelectCrewCharacter(Character character, GUIComponent frame) + public bool SelectCrewCharacter(Character character, GUIComponent frame) { if (character == null) { return false; } @@ -3473,9 +3260,9 @@ namespace Barotrauma.Networking return true; } - public virtual bool SelectCrewClient(Client client, GUIComponent frame) + public bool SelectCrewClient(Client client, GUIComponent frame) { - if (client == null || client.ID == ID) { return false; } + if (client == null || client.SessionId == SessionId) { return false; } CreateSelectionRelatedButtons(client, frame); return true; } @@ -3538,7 +3325,7 @@ namespace Barotrauma.Networking OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } }; } - else if (serverSettings.AllowVoteKick && client.AllowKicking) + else if (ServerSettings.AllowVoteKick && client.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), TextManager.Get("VoteToKick"), style: "GUIButtonSmall") @@ -3546,14 +3333,10 @@ namespace Barotrauma.Networking 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) + public void CreateKickReasonPrompt(string clientName, bool ban) { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), @@ -3614,11 +3397,11 @@ namespace Barotrauma.Networking if (!permaBanTickBox.Selected) { TimeSpan banDuration = new TimeSpan(durationInputDays.IntValue, durationInputHours.IntValue, 0, 0); - BanPlayer(clientName, banReasonBox.Text, ban, banDuration); + BanPlayer(clientName, banReasonBox.Text, banDuration); } else { - BanPlayer(clientName, banReasonBox.Text, range: rangeBan); + BanPlayer(clientName, banReasonBox.Text); } } else @@ -3631,39 +3414,36 @@ namespace Barotrauma.Networking banReasonPrompt.Buttons[1].OnClicked += banReasonPrompt.Close; } - public void ReportError(ClientNetError error, UInt16 expectedID = 0, UInt16 eventID = 0, UInt16 entityID = 0) + public void ReportError(ClientNetError error, UInt16 expectedId = 0, UInt16 eventId = 0, UInt16 entityId = 0) { IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)ClientPacketHeader.ERROR); - outMsg.Write((byte)error); + outMsg.WriteByte((byte)ClientPacketHeader.ERROR); + outMsg.WriteByte((byte)error); switch (error) { case ClientNetError.MISSING_EVENT: - outMsg.Write(expectedID); - outMsg.Write(eventID); + outMsg.WriteUInt16(expectedId); + outMsg.WriteUInt16(eventId); break; case ClientNetError.MISSING_ENTITY: - outMsg.Write(eventID); - outMsg.Write(entityID); - outMsg.Write((byte)Submarine.Loaded.Count); + outMsg.WriteUInt16(eventId); + outMsg.WriteUInt16(entityId); + outMsg.WriteByte((byte)Submarine.Loaded.Count); foreach (Submarine sub in Submarine.Loaded) { - outMsg.Write(sub.Info.Name); + outMsg.WriteString(sub.Info.Name); } break; } - clientPeer.Send(outMsg, DeliveryMethod.Reliable); + ClientPeer.Send(outMsg, DeliveryMethod.Reliable); - if (!eventErrorWritten) - { - WriteEventErrorData(error, expectedID, eventID, entityID); - eventErrorWritten = true; - } + WriteEventErrorData(error, expectedId, eventId, entityId); } private bool eventErrorWritten; private void WriteEventErrorData(ClientNetError error, UInt16 expectedID, UInt16 eventID, UInt16 entityID) { + if (eventErrorWritten) { return; } List errorLines = new List { error.ToString(), "" @@ -3753,12 +3533,14 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } File.WriteAllLines(filePath, errorLines); + + eventErrorWritten = true; } #if DEBUG public void ForceTimeOut() { - clientPeer?.ForceTimeOut(); + ClientPeer?.ForceTimeOut(); } #endif } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 50486ace0..4a46f3c9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -103,7 +103,7 @@ namespace Barotrauma.Networking eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now; } - msg.Write((byte)ClientNetObject.ENTITY_STATE); + msg.WriteByte((byte)ClientNetObject.ENTITY_STATE); Write(msg, eventsToSync, out _); } @@ -203,7 +203,7 @@ namespace Barotrauma.Networking DebugConsole.NewMessage( "Received msg " + thisEventID + ", entity " + entityID + " not found", GUIStyle.Red); - GameMain.Client.ReportError(ClientNetError.MISSING_ENTITY, eventID: thisEventID, entityID: entityID); + GameMain.Client.ReportError(ClientNetError.MISSING_ENTITY, eventId: thisEventID, entityId: entityID); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs index 0bca588f9..f474cd2fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg) { - msg.Write(CharacterStateID); + msg.WriteUInt16(CharacterStateID); serializable.ClientEventWrite(msg, Data); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 673a8423f..b21592b8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -6,8 +6,8 @@ namespace Barotrauma.Networking { public override void ClientWrite(IWriteMessage msg) { - msg.Write((byte)ClientNetObject.CHAT_MESSAGE); - msg.Write(NetStateID); + msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); WriteOrder(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index f39baaaec..b2cf3a633 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -1,164 +1,199 @@ -using Barotrauma.Extensions; +#nullable enable using Barotrauma.Steam; using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; -using System.Text; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { - abstract class ClientPeer + internal abstract class ClientPeer { - public class ServerContentPackage - { - public readonly string Name; - public readonly Md5Hash Hash; - public readonly UInt64 WorkshopId; - public readonly DateTime InstallTime; - - public RegularPackage RegularPackage - { - get - { - return ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); - } - } - - public CorePackage CorePackage - { - get - { - return ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); - } - } - - public ContentPackage ContentPackage - => (ContentPackage)RegularPackage ?? CorePackage; - - - public string GetPackageStr() - => $"\"{Name}\" (hash {Hash.ShortRepresentation})"; - - public ServerContentPackage(string name, Md5Hash hash, UInt64 workshopId, DateTime installTime) - { - Name = name; - Hash = hash; - WorkshopId = workshopId; - InstallTime = installTime; - } - } - public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; - public delegate void MessageCallback(IReadMessage message); - public delegate void DisconnectCallback(bool disableReconnect); - public delegate void DisconnectMessageCallback(string message); - public delegate void PasswordCallback(int salt, int retries); - public delegate void InitializationCompleteCallback(); - - public MessageCallback OnMessageReceived; - public DisconnectCallback OnDisconnect; - public DisconnectMessageCallback OnDisconnectMessageReceived; - public PasswordCallback OnRequestPassword; - public InitializationCompleteCallback OnInitializationComplete; + public readonly record struct Callbacks( + Callbacks.MessageCallback OnMessageReceived, + Callbacks.DisconnectCallback OnDisconnect, + Callbacks.InitializationCompleteCallback OnInitializationComplete) + { + public delegate void MessageCallback(IReadMessage message); + public delegate void DisconnectCallback(PeerDisconnectPacket disconnectPacket); + public delegate void InitializationCompleteCallback(); + } - public string Name; + protected readonly Callbacks callbacks; - public string Version { get; protected set; } + public readonly Endpoint ServerEndpoint; + public NetworkConnection? ServerConnection { get; protected set; } - public NetworkConnection ServerConnection { get; protected set; } + protected readonly bool isOwner; + protected readonly Option ownerKey; - public abstract void Start(object endPoint, int ownerKey); - public abstract void Close(string msg = null, bool disableReconnect = false); + protected bool isActive; + + public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) + { + ServerEndpoint = serverEndpoint; + this.callbacks = callbacks; + this.ownerKey = ownerKey; + isOwner = ownerKey.IsSome(); + } + + public abstract void Start(); + public abstract void Close(PeerDisconnectPacket peerDisconnectPacket); public abstract void Update(float deltaTime); public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void SendPassword(string password); - protected abstract void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg); + protected abstract void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body); protected ConnectionInitialization initializationStep; - protected bool contentPackageOrderReceived; - protected int ownerKey = 0; + public bool ContentPackageOrderReceived { get; set; } protected int passwordSalt; - protected Steamworks.AuthTicket steamAuthTicket; - protected void ReadConnectionInitializationStep(IReadMessage inc) + protected Steamworks.AuthTicket? steamAuthTicket; + private GUIMessageBox? passwordMsgBox; + + public bool WaitingForPassword + => isActive && initializationStep == ConnectionInitialization.Password + && passwordMsgBox != null + && GUIMessageBox.MessageBoxes.Contains(passwordMsgBox); + + public struct IncomingInitializationMessage { - ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); + public ConnectionInitialization InitializationStep; + public IReadMessage Message; + } - IWriteMessage outMsg; - - switch (step) + protected void ReadConnectionInitializationStep(IncomingInitializationMessage inc) + { + switch (inc.InitializationStep) { case ConnectionInitialization.SteamTicketAndVersion: + { if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } - outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); - outMsg.Write(Name); - outMsg.Write(ownerKey); - outMsg.Write(SteamManager.GetSteamID()); - if (steamAuthTicket == null) - { - outMsg.Write((UInt16)0); - } - else - { - outMsg.Write((UInt16)steamAuthTicket.Data.Length); - outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); - } - outMsg.Write(GameMain.Version.ToString()); - outMsg.Write(GameSettings.CurrentConfig.Language.Value); - SendMsgInternal(DeliveryMethod.Reliable, outMsg); + PeerPacketHeaders headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.SteamTicketAndVersion + }; + + ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket + { + Name = GameMain.Client.Name, + OwnerKey = ownerKey, + SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), + SteamAuthTicket = steamAuthTicket switch + { + null => Option.None(), + var ticket => Option.Some(ticket.Data) + }, + GameVersion = GameMain.Version.ToString(), + Language = GameSettings.CurrentConfig.Language.Value + }; + + SendMsgInternal(headers, body); break; + } case ConnectionInitialization.ContentPackageOrder: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || - initializationStep == ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; } + { + if (initializationStep + is ConnectionInitialization.SteamTicketAndVersion + or ConnectionInitialization.Password) + { + initializationStep = ConnectionInitialization.ContentPackageOrder; + } + if (initializationStep != ConnectionInitialization.ContentPackageOrder) { return; } - outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.ContentPackageOrder); - string serverName = inc.ReadString(); - - UInt32 packageCount = inc.ReadVariableUInt32(); - List serverPackages = new List(); - for (int i = 0; i < packageCount; i++) + PeerPacketHeaders headers = new PeerPacketHeaders { - string name = inc.ReadString(); - UInt32 hashByteCount = inc.ReadVariableUInt32(); - byte[] hashBytes = inc.ReadBytes((int)hashByteCount); - UInt64 workshopId = inc.ReadUInt64(); - UInt32 installTimeDiffSeconds = inc.ReadUInt32(); - DateTime installTime = DateTime.UtcNow + TimeSpan.FromSeconds(installTimeDiffSeconds); + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.ContentPackageOrder + }; - var pkg = new ServerContentPackage(name, Md5Hash.BytesAsHash(hashBytes), workshopId, installTime); - serverPackages.Add(pkg); + var orderPacket = INetSerializableStruct.Read(inc.Message); + + if (!ContentPackageOrderReceived) + { + ServerContentPackages = orderPacket.ContentPackages; + if (ServerContentPackages.Length == 0) + { + string errorMsg = "Error in ContentPackageOrder message: list of content packages enabled on the server was empty."; + GameAnalyticsManager.AddErrorEventOnce("ClientPeer.ReadConnectionInitializationStep:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); + } + ContentPackageOrderReceived = true; + + SendMsgInternal(headers, null); } - if (!contentPackageOrderReceived) - { - ServerContentPackages = serverPackages.ToImmutableArray(); - SendMsgInternal(DeliveryMethod.Reliable, outMsg); - } break; + } case ConnectionInitialization.Password: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) + { + initializationStep = ConnectionInitialization.Password; + } + if (initializationStep != ConnectionInitialization.Password) { return; } - bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); - int retries = 0; - if (incomingSalt) + + var passwordPacket = INetSerializableStruct.Read(inc.Message); + + if (WaitingForPassword) { return; } + + passwordPacket.Salt.TryUnwrap(out passwordSalt); + passwordPacket.RetriesLeft.TryUnwrap(out var retries); + + LocalizedString pwMsg = TextManager.Get("PasswordRequired"); + + passwordMsgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) { - passwordSalt = inc.ReadInt32(); - } - else + Censor = true + }; + + if (retries > 0) { - retries = inc.ReadInt32(); + var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); + incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); + passwordHolder.Recalculate(); } - OnRequestPassword?.Invoke(passwordSalt, retries); + + passwordMsgBox.Content.Recalculate(); + passwordMsgBox.Content.RectTransform.MinSize = new Point(0, passwordMsgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); + passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y)); + + var okButton = passwordMsgBox.Buttons[0]; + okButton.OnClicked += (_, __) => + { + SendPassword(passwordBox.Text); + return true; + }; + okButton.OnClicked += passwordMsgBox.Close; + + var cancelButton = passwordMsgBox.Buttons[1]; + cancelButton.OnClicked = (_, __) => + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + passwordMsgBox?.Close(); passwordMsgBox = null; + + return true; + }; + + passwordBox.OnEnterPressed += (_, __) => + { + okButton.OnClicked.Invoke(okButton, okButton.UserData); + return true; + }; + + passwordBox.Select(); + break; } } @@ -167,4 +202,4 @@ namespace Barotrauma.Networking public abstract void ForceTimeOut(); #endif } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index ed3738964..a95afd553 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -1,75 +1,82 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Net; using System.Text; using Lidgren.Network; using Barotrauma.Steam; -using System.Linq; namespace Barotrauma.Networking { - class LidgrenClientPeer : ClientPeer + internal sealed class LidgrenClientPeer : ClientPeer { - private bool isActive; - private NetClient netClient; - private NetPeerConfiguration netPeerConfiguration; + private NetClient? netClient; + private readonly NetPeerConfiguration netPeerConfiguration; - List incomingLidgrenMessages; + private readonly List incomingLidgrenMessages; - public LidgrenClientPeer(string name) + private LidgrenEndpoint lidgrenEndpoint => + ServerConnection is LidgrenConnection { Endpoint: LidgrenEndpoint result } + ? result + : throw new InvalidOperationException(); + + public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option ownerKey) : base(endpoint, callbacks, ownerKey) { ServerConnection = null; - Name = name; - netClient = null; isActive = false; - } - - public override void Start(object endPoint, int ownerKey) - { - if (isActive) { return; } - - this.ownerKey = ownerKey; - - contentPackageOrderReceived = false; netPeerConfiguration = new NetPeerConfiguration("barotrauma") { UseDualModeSockets = GameSettings.CurrentConfig.UseDualModeSockets }; - netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt - | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); + netPeerConfiguration.DisableMessageType( + NetIncomingMessageType.DebugMessage + | NetIncomingMessageType.WarningMessage + | NetIncomingMessageType.Receipt + | NetIncomingMessageType.ErrorMessage + | NetIncomingMessageType.Error); + + incomingLidgrenMessages = new List(); + } + + public override void Start() + { + if (isActive) { return; } + + incomingLidgrenMessages.Clear(); + + ContentPackageOrderReceived = false; netClient = new NetClient(netPeerConfiguration); if (SteamManager.IsInitialized) { steamAuthTicket = SteamManager.GetAuthSessionTicket(); - //TODO: wait for GetAuthSessionTicketResponse_t - if (steamAuthTicket == null) { throw new Exception("GetAuthSessionTicket returned null"); } } - incomingLidgrenMessages = new List(); - initializationStep = ConnectionInitialization.SteamTicketAndVersion; - if (!(endPoint is IPEndPoint ipEndPoint)) + if (!(ServerEndpoint is LidgrenEndpoint lidgrenEndpointValue)) { - throw new InvalidCastException("endPoint is not IPEndPoint"); + throw new InvalidCastException($"Endpoint is not {nameof(LidgrenEndpoint)}"); } + if (ServerConnection != null) { throw new InvalidOperationException("ServerConnection is not null"); } netClient.Start(); - ServerConnection = new LidgrenConnection("Server", netClient.Connect(ipEndPoint), 0) + + var netConnection = netClient.Connect(lidgrenEndpointValue.NetEndpoint); + + ServerConnection = new LidgrenConnection(netConnection) { Status = NetworkConnectionStatus.Connected }; @@ -81,11 +88,18 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ownerKey != 0 && (ChildServerRelay.Process?.HasExited ?? true)) + ToolBox.ThrowIfNull(netClient); + ToolBox.ThrowIfNull(incomingLidgrenMessages); + + if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) { - Close(); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; + msgBox.Buttons[0].OnClicked += (btn, obj) => + { + GameMain.MainMenuScreen.Select(); + return false; + }; return; } @@ -97,7 +111,11 @@ namespace Barotrauma.Networking foreach (NetIncomingMessage inc in incomingLidgrenMessages) { - if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } + if (!inc.SenderConnection.RemoteEndPoint.Equals(lidgrenEndpoint.NetEndpoint)) + { + DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}"); + continue; + } switch (inc.MessageType) { @@ -111,26 +129,36 @@ namespace Barotrauma.Networking } } - private void HandleDataMessage(NetIncomingMessage inc) + private void HandleDataMessage(NetIncomingMessage lidgrenMsg) { if (!isActive) { return; } - PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); + ToolBox.ThrowIfNull(ServerConnection); - if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) + IReadMessage inc = lidgrenMsg.ToReadMessage(); + + var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); + + if (packetHeader.IsConnectionInitializationStep()) { - ReadConnectionInitializationStep(new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); + if (initializationStep == ConnectionInitialization.Success) { return; } + + ReadConnectionInitializationStep(new IncomingInitializationMessage + { + InitializationStep = initialization ?? throw new Exception("Initialization step missing"), + Message = inc + }); } else { if (initializationStep != ConnectionInitialization.Success) { - OnInitializationComplete?.Invoke(); + callbacks.OnInitializationComplete.Invoke(); initializationStep = ConnectionInitialization.Success; } - UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Data, packetHeader.IsCompressed(), inc.PositionInBytes, length, ServerConnection); - OnMessageReceived?.Invoke(msg); + + var packet = INetSerializableStruct.Read(inc); + callbacks.OnMessageReceived.Invoke(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection)); } } @@ -138,13 +166,14 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - NetConnectionStatus status = (NetConnectionStatus)inc.ReadByte(); + NetConnectionStatus status = inc.ReadHeader(); switch (status) { case NetConnectionStatus.Disconnected: string disconnectMsg = inc.ReadString(); - Close(disconnectMsg); - OnDisconnectMessageReceived?.Invoke(disconnectMsg); + var peerDisconnectPacket = + PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg); + Close(peerDisconnectPacket.Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown))); break; } } @@ -153,49 +182,47 @@ namespace Barotrauma.Networking { if (!isActive) { return; } + ToolBox.ThrowIfNull(netClient); + if (initializationStep != ConnectionInitialization.Password) { return; } - NetOutgoingMessage outMsg = netClient.CreateMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.Password); - byte[] saltedPw = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt); - outMsg.Write((byte)saltedPw.Length); - outMsg.Write(saltedPw, 0, saltedPw.Length); - NetSendResult result = netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); - if (result != NetSendResult.Queued && result != NetSendResult.Sent) + + var headers = new PeerPacketHeaders { - DebugConsole.NewMessage("Failed to send " + initializationStep.ToString() + " message to host: " + result); - } + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.Password + }; + var body = new ClientPeerPasswordPacket + { + Password = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt) + }; + + SendMsgInternal(headers, body); } - public override void Close(string msg = null, bool disableReconnect = false) + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } + ToolBox.ThrowIfNull(netClient); + isActive = false; - netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); + netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation()); netClient = null; - steamAuthTicket?.Cancel(); steamAuthTicket = null; - OnDisconnect?.Invoke(disableReconnect); + + steamAuthTicket?.Cancel(); + steamAuthTicket = null; + + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } - NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - switch (deliveryMethod) - { - case DeliveryMethod.Unreliable: - lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - break; - case DeliveryMethod.Reliable: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; - break; - case DeliveryMethod.ReliableOrdered: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; - break; - } + ToolBox.ThrowIfNull(netClient); + ToolBox.ThrowIfNull(netPeerConfiguration); #if DEBUG netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Client.SimulatedDuplicatesChance; @@ -204,30 +231,42 @@ namespace Barotrauma.Networking netPeerConfiguration.SimulatedLoss = GameMain.Client.SimulatedLoss; #endif - NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); - byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); - lidgrenMsg.Write((UInt16)length); - lidgrenMsg.Write(msgData, 0, length); + byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); - NetSendResult result = netClient.SendMessage(lidgrenMsg, lidgrenDeliveryMethod); + var headers = new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None, + Initialization = null + }; + var body = new PeerPacketMessage + { + Buffer = bufAux + }; + + SendMsgInternal(headers, body); + } + + protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) + { + ToolBox.ThrowIfNull(netClient); + + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteNetSerializableStruct(headers); + body?.Write(msg); + + NetSendResult result = ForwardToLidgren(msg, DeliveryMethod.Reliable); if (result != NetSendResult.Queued && result != NetSendResult.Sent) { - DebugConsole.NewMessage("Failed to send message to host: " + result); + DebugConsole.NewMessage($"Failed to send message to host: {result}\n{Environment.StackTrace}"); } } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + private NetSendResult ForwardToLidgren(IWriteMessage msg, DeliveryMethod deliveryMethod) { - NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); - lidgrenMsg.Write(msg.Buffer, 0, msg.LengthBytes); + ToolBox.ThrowIfNull(netClient); - NetSendResult result = netClient.SendMessage(lidgrenMsg, NetDeliveryMethod.ReliableUnordered); - if (result != NetSendResult.Queued && result != NetSendResult.Sent) - { - DebugConsole.NewMessage("Failed to send message to host: " + result + "\n" + Environment.StackTrace); - } + return netClient.SendMessage(msg.ToLidgren(netClient), deliveryMethod.ToLidgren()); } #if DEBUG @@ -237,4 +276,4 @@ namespace Barotrauma.Networking } #endif } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 6e097aabf..35e579d2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -1,38 +1,44 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Text; using System.Linq; +using System.Text; using Barotrauma.Steam; using System.Threading; -using Barotrauma.Items.Components; namespace Barotrauma.Networking { - class SteamP2PClientPeer : ClientPeer + internal sealed class SteamP2PClientPeer : ClientPeer { - private bool isActive; - private UInt64 hostSteamId; + private readonly SteamId hostSteamId; private double timeout; private double heartbeatTimer; private double connectionStatusTimer; private long sentBytes, receivedBytes; - private List incomingInitializationMessages; - private List incomingDataMessages; + private readonly List incomingInitializationMessages = new List(); + private readonly List incomingDataMessages = new List(); - public SteamP2PClientPeer(string name) + public SteamP2PClientPeer(SteamP2PEndpoint endpoint, Callbacks callbacks) : base(endpoint, callbacks, Option.None()) { ServerConnection = null; - Name = name; - isActive = false; + + if (!(ServerEndpoint is SteamP2PEndpoint steamIdEndpoint)) + { + throw new InvalidCastException("endPoint is not SteamId"); + } + + hostSteamId = steamIdEndpoint.SteamId; } - public override void Start(object endPoint, int ownerKey) + public override void Start() { - contentPackageOrderReceived = false; + if (isActive) { return; } + + ContentPackageOrderReceived = false; steamAuthTicket = SteamManager.GetAuthSessionTicket(); //TODO: wait for GetAuthSessionTicketResponse_t @@ -42,32 +48,22 @@ namespace Barotrauma.Networking throw new Exception("GetAuthSessionTicket returned null"); } - if (!(endPoint is UInt64 steamIdEndpoint)) - { - throw new InvalidCastException("endPoint is not UInt64"); - } - - hostSteamId = steamIdEndpoint; - Steamworks.SteamNetworking.ResetActions(); Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection; Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed; Steamworks.SteamNetworking.AllowP2PPacketRelay(true); - ServerConnection = new SteamP2PConnection("Server", hostSteamId); - ServerConnection.SetOwnerSteamIDIfUnknown(hostSteamId); + ServerConnection = new SteamP2PConnection(hostSteamId); + ServerConnection.SetAccountInfo(new AccountInfo(hostSteamId)); - incomingInitializationMessages = new List(); - incomingDataMessages = new List(); - - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.ConnectionStarted); - - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.ConnectionStarted + }; + SendMsgInternal(headers, null); initializationStep = ConnectionInitialization.SteamTicketAndVersion; @@ -81,7 +77,8 @@ namespace Barotrauma.Networking private void OnIncomingConnection(Steamworks.SteamId steamId) { if (!isActive) { return; } - if (steamId == hostSteamId) + + if (steamId == hostSteamId.Value) { Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); } @@ -89,44 +86,51 @@ namespace Barotrauma.Networking initializationStep != ConnectionInitialization.ContentPackageOrder && initializationStep != ConnectionInitialization.Success) { - DebugConsole.ThrowError($"Connection from incorrect SteamID was rejected: "+ - $"expected {SteamManager.SteamIDUInt64ToString(hostSteamId)}," + - $"got {SteamManager.SteamIDUInt64ToString(steamId)}"); + DebugConsole.ThrowError("Connection from incorrect SteamID was rejected: " + + $"expected {hostSteamId}," + + $"got {new SteamId(steamId)}"); } } private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error) { if (!isActive) { return; } - if (steamId != hostSteamId) { return; } - Close($"SteamP2P connection failed: {error}"); - OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); + + if (steamId != hostSteamId.Value) { return; } + + Close(PeerDisconnectPacket.SteamP2PError(error)); } private void OnP2PData(ulong steamId, byte[] data, int dataLength) { if (!isActive) { return; } - if (steamId != hostSteamId) { return; } - timeout = Screen.Selected == GameMain.GameScreen ? - NetworkConnection.TimeoutThresholdInGame : - NetworkConnection.TimeoutThreshold; - - PacketHeader packetHeader = (PacketHeader)data[0]; + if (steamId != hostSteamId.Value) { return; } + + timeout = Screen.Selected == GameMain.GameScreen + ? NetworkConnection.TimeoutThresholdInGame + : NetworkConnection.TimeoutThreshold; + + IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); + + var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); if (!packetHeader.IsServerMessage()) { return; } if (packetHeader.IsConnectionInitializationStep()) { - ulong low = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8); - ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8 + 32); - ulong lobbyId = low + (high << 32); + if (!initialization.HasValue) { return; } - Steam.SteamManager.JoinLobby(lobbyId, false); - IReadMessage inc = new ReadOnlyMessage(data, false, 1 + 8, dataLength - (1 + 8), ServerConnection); + var relayPacket = INetSerializableStruct.Read(inc); + + SteamManager.JoinLobby(relayPacket.LobbyID, false); if (initializationStep != ConnectionInitialization.Success) { - incomingInitializationMessages.Add(inc); + incomingInitializationMessages.Add(new IncomingInitializationMessage + { + InitializationStep = initialization.Value, + Message = relayPacket.Message.GetReadMessageUncompressed() + }); } } else if (packetHeader.IsHeartbeatMessage()) @@ -135,17 +139,13 @@ namespace Barotrauma.Networking } else if (packetHeader.IsDisconnectMessage()) { - IReadMessage inc = new ReadOnlyMessage(data, false, 1, dataLength - 1, ServerConnection); - string msg = inc.ReadString(); - Close(msg); - OnDisconnectMessageReceived?.Invoke(msg); + PeerDisconnectPacket packet = INetSerializableStruct.Read(inc); + Close(packet); } else { - UInt16 length = Lidgren.Network.NetBitWriter.ReadUInt16(data, 16, 8); - - IReadMessage inc = new ReadOnlyMessage(data, packetHeader.IsCompressed(), 3, length, ServerConnection); - incomingDataMessages.Add(inc); + var packet = INetSerializableStruct.Read(inc); + incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!)); } } @@ -157,6 +157,7 @@ namespace Barotrauma.Networking { timeout -= deltaTime; } + heartbeatTimer -= deltaTime; if (initializationStep != ConnectionInitialization.Password && @@ -166,20 +167,18 @@ namespace Barotrauma.Networking connectionStatusTimer -= deltaTime; if (connectionStatusTimer <= 0.0) { - var state = Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId); - if (state == null) + if (Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value) is { } state) { - Close("SteamP2P connection could not be established"); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PError.ToString()); + if (state.P2PSessionError != Steamworks.P2PSessionError.None) + { + Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError)); + } } else { - if (state?.P2PSessionError != Steamworks.P2PSessionError.None) - { - Close($"SteamP2P error code: {state?.P2PSessionError}"); - OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state?.P2PSessionError}"); - } + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } + connectionStatusTimer = 1.0f; } } @@ -187,11 +186,13 @@ namespace Barotrauma.Networking for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } + var packet = Steamworks.SteamNetworking.ReadP2PPacket(); - if (packet.HasValue) + if (packet is { SteamId: var steamId, Data: var data }) { - OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0); - receivedBytes += packet?.Data.Length ?? 0; + OnP2PData(steamId, data, data.Length); + if (!isActive) { return; } + receivedBytes += data.Length; } } @@ -200,20 +201,18 @@ namespace Barotrauma.Networking if (heartbeatTimer < 0.0) { - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Unreliable); - outMsg.Write((byte)PacketHeader.IsHeartbeatMessage); - - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Unreliable); - sentBytes += outMsg.LengthBytes; - - heartbeatTimer = 5.0; + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Unreliable, + PacketHeader = PacketHeader.IsHeartbeatMessage, + Initialization = null + }; + SendMsgInternal(headers, null); } if (timeout < 0.0) { - Close("Timed out"); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut)); return; } @@ -221,12 +220,33 @@ namespace Barotrauma.Networking { if (incomingDataMessages.Count > 0) { - OnInitializationComplete?.Invoke(); + void initializationError(string errorMsg, string analyticsTag) + { + GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + } + + if (!ContentPackageOrderReceived) + { + initializationError( + errorMsg: "Error during connection initialization: completed initialization before receiving content package order.", + analyticsTag: "ContentPackageOrderNotReceived"); + return; + } + if (ServerContentPackages.Length == 0) + { + initializationError( + errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.", + analyticsTag: "NoContentPackages"); + return; + } + callbacks.OnInitializationComplete.Invoke(); initializationStep = ConnectionInitialization.Success; } else { - foreach (IReadMessage inc in incomingInitializationMessages) + foreach (var inc in incomingInitializationMessages) { ReadConnectionInitializationStep(inc); } @@ -237,7 +257,7 @@ namespace Barotrauma.Networking { foreach (IReadMessage inc in incomingDataMessages) { - OnMessageReceived?.Invoke(inc); + callbacks.OnMessageReceived.Invoke(inc); } } @@ -249,76 +269,40 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - byte[] buf = new byte[msg.LengthBytes + 4]; - buf[0] = (byte)deliveryMethod; + byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); - byte[] bufAux = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref bufAux, compressPastThreshold, out bool isCompressed, out int length); - - buf[1] = (byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None); - - buf[2] = (byte)(length & 0xff); - buf[3] = (byte)((length >> 8) & 0xff); - - Array.Copy(bufAux, 0, buf, 4, length); - - Steamworks.P2PSend sendType; - switch (deliveryMethod) + var headers = new PeerPacketHeaders { - case DeliveryMethod.Reliable: - case DeliveryMethod.ReliableOrdered: - //the documentation seems to suggest that the Reliable send type - //enforces packet order (TODO: verify) - sendType = Steamworks.P2PSend.Reliable; - break; - default: - sendType = Steamworks.P2PSend.Unreliable; - break; - } - - if (length + 8 >= MsgConstants.MTU) + DeliveryMethod = deliveryMethod, + PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None, + Initialization = null + }; + var body = new PeerPacketMessage { - DebugConsole.Log("WARNING: message length comes close to exceeding MTU, forcing reliable send (" + length.ToString() + " bytes)"); - sendType = Steamworks.P2PSend.Reliable; - } + Buffer = bufAux + }; heartbeatTimer = 5.0; + // Using an extra local method here to reduce chance of error whenever we need to change this + void performSend() => SendMsgInternal(headers, body); #if DEBUG CoroutineManager.Invoke(() => - { - if (GameMain.Client == null) { return; } - if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && sendType != Steamworks.P2PSend.Reliable) { return; } - int count = Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedDuplicatesChance ? 2 : 1; - for (int i = 0; i < count; i++) { - Send(buf, length + 4, sendType); - } - }, - GameMain.Client.SimulatedMinimumLatency + Rand.Range(0.0f, GameMain.Client.SimulatedRandomLatency)); -#else - Send(buf, length + 4, sendType); -#endif - } + if (GameMain.Client == null) { return; } - private void Send(byte[] buf, int length, Steamworks.P2PSend sendType) - { - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); - sentBytes += length + 4; - if (!successSend) - { - if (sendType != Steamworks.P2PSend.Reliable) - { - DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + length.ToString() + " bytes)"); - sendType = Steamworks.P2PSend.Reliable; - successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); - sentBytes += length + 4; - } - if (!successSend) - { - DebugConsole.AddWarning("Failed to send message to remote peer! (" + length.ToString() + " bytes)"); - } - } + if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && deliveryMethod is DeliveryMethod.Unreliable) { return; } + + int count = Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedDuplicatesChance ? 2 : 1; + for (int i = 0; i < count; i++) + { + performSend(); + } + }, + GameMain.Client.SimulatedMinimumLatency + Rand.Range(0.0f, GameMain.Client.SimulatedRandomLatency)); +#else + performSend(); +#endif } public override void SendPassword(string password) @@ -326,20 +310,22 @@ namespace Barotrauma.Networking if (!isActive) { return; } if (initializationStep != ConnectionInitialization.Password) { return; } - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.Password); - byte[] saltedPw = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt); - outMsg.Write((byte)saltedPw.Length); - outMsg.Write(saltedPw, 0, saltedPw.Length); - heartbeatTimer = 5.0; - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.Password + }; + var body = new ClientPeerPasswordPacket + { + Password = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt) + }; + + SendMsgInternal(headers, body); } - - public override void Close(string msg = null, bool disableReconnect = false) + + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } @@ -347,55 +333,54 @@ namespace Barotrauma.Networking isActive = false; - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)PacketHeader.IsDisconnectMessage); - outMsg.Write(msg ?? "Disconnected"); - - try + var headers = new PeerPacketHeaders { - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to send a disconnect message to the server using SteamP2P.", e); - } + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsDisconnectMessage, + Initialization = null + }; + SendMsgInternal(headers, peerDisconnectPacket); Thread.Sleep(100); Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId); + Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value); - steamAuthTicket?.Cancel(); steamAuthTicket = null; - hostSteamId = 0; + steamAuthTicket?.Cancel(); + steamAuthTicket = null; - OnDisconnect?.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) { - Steamworks.P2PSend sendType; - switch (deliveryMethod) + IWriteMessage msgToSend = new WriteOnlyMessage(); + msgToSend.WriteNetSerializableStruct(headers); + body?.Write(msgToSend); + ForwardToSteamP2P(msgToSend, headers.DeliveryMethod); + } + + private void ForwardToSteamP2P(IWriteMessage msg, DeliveryMethod deliveryMethod) + { + heartbeatTimer = 5.0; + int length = msg.LengthBytes; + + bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, deliveryMethod.ToSteam()); + sentBytes += length; + + if (successSend) { return; } + + if (deliveryMethod is DeliveryMethod.Unreliable) { - case DeliveryMethod.Reliable: - case DeliveryMethod.ReliableOrdered: - //the documentation seems to suggest that the Reliable send type - //enforces packet order (TODO: verify) - sendType = Steamworks.P2PSend.Reliable; - break; - default: - sendType = Steamworks.P2PSend.Unreliable; - break; + DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)"); + successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, DeliveryMethod.Reliable.ToSteam()); + sentBytes += length; } - IWriteMessage msgToSend = new WriteOnlyMessage(); - msgToSend.Write((byte)deliveryMethod); - msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); - - heartbeatTimer = 5.0; - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, msgToSend.Buffer, msgToSend.LengthBytes, 0, sendType); - sentBytes += msg.LengthBytes; + if (!successSend) + { + DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)"); + } } #if DEBUG @@ -405,4 +390,4 @@ namespace Barotrauma.Networking } #endif } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 2f5c2f480..48fe24b0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -1,75 +1,82 @@ -using Barotrauma.Steam; +#nullable enable +using Barotrauma.Steam; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; +using Barotrauma.Extensions; namespace Barotrauma.Networking { - class SteamP2POwnerPeer : ClientPeer + sealed class SteamP2POwnerPeer : ClientPeer { - private bool isActive; + private readonly SteamId selfSteamID; + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - private readonly UInt64 selfSteamID; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey); + private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); + private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64); - private UInt64 ReadSteamId(IReadMessage inc) - => inc.ReadUInt64() ^ ownerKey64; - private void WriteSteamId(IWriteMessage msg, UInt64 val) - => msg.Write(val ^ ownerKey64); - private long sentBytes, receivedBytes; - class RemotePeer + private sealed class RemotePeer { - public UInt64 SteamID; - public UInt64 OwnerSteamID; + public readonly SteamId SteamId; + public Option OwnerSteamId; public double? DisconnectTime; public bool Authenticating; public bool Authenticated; - public class UnauthedMessage + public readonly struct UnauthedMessage { - public DeliveryMethod DeliveryMethod; - public IWriteMessage Message; - } - public List UnauthedMessages; + public readonly SteamId Sender; + public readonly byte[] Bytes; + public readonly int Length; - public RemotePeer(UInt64 steamId) + public UnauthedMessage(SteamId sender, byte[] bytes) + { + Sender = sender; + Bytes = bytes; + Length = bytes.Length; + } + } + + public readonly List UnauthedMessages; + + public RemotePeer(SteamId steamId) { - SteamID = steamId; - OwnerSteamID = 0; + SteamId = steamId; + OwnerSteamId = Option.None(); DisconnectTime = null; Authenticating = false; Authenticated = false; UnauthedMessages = new List(); } - } - List remotePeers; - public SteamP2POwnerPeer(string name) + private List remotePeers = null!; + + public SteamP2POwnerPeer(Callbacks callbacks, int ownerKey) : base(new PipeEndpoint(), callbacks, Option.Some(ownerKey)) { ServerConnection = null; - Name = name; - isActive = false; - selfSteamID = Steam.SteamManager.GetSteamID(); + selfSteamID = SteamManager.GetSteamId().TryUnwrap(out var steamId) + ? steamId + : throw new InvalidOperationException("Steamworks not initialized"); } - public override void Start(object endPoint, int ownerKey) + + public override void Start() { if (isActive) { return; } - this.ownerKey = ownerKey; - initializationStep = ConnectionInitialization.SteamTicketAndVersion; - ServerConnection = new PipeConnection(selfSteamID); - ServerConnection.Status = NetworkConnectionStatus.Connected; + ServerConnection = new PipeConnection(selfSteamID) + { + Status = NetworkConnectionStatus.Connected + }; remotePeers = new List(); @@ -82,47 +89,34 @@ namespace Barotrauma.Networking isActive = true; } - private void OnAuthChange(Steamworks.SteamId steamID, Steamworks.SteamId ownerID, Steamworks.AuthResponse status) + private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) { - RemotePeer remotePeer = remotePeers.Find(p => p.SteamID == steamID); - DebugConsole.Log(steamID + " validation: " + status + ", " + (remotePeer != null)); + RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); if (remotePeer == null) { return; } - if (remotePeer.Authenticated) - { - if (status != Steamworks.AuthResponse.OK) - { - DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); - } - return; - } - if (status == Steamworks.AuthResponse.OK) { - remotePeer.OwnerSteamID = ownerID; + if (remotePeer.Authenticated) { return; } + + SteamId ownerSteamId = new SteamId(ownerId); + remotePeer.OwnerSteamId = Option.Some(ownerSteamId); remotePeer.Authenticated = true; remotePeer.Authenticating = false; - foreach (var msg in remotePeer.UnauthedMessages) + foreach (var unauthedMessage in remotePeer.UnauthedMessages) { - //rewrite the owner id before - //forwarding the messages to - //the server, since it's only - //known now - int prevBitPosition = msg.Message.BitPosition; - msg.Message.BitPosition = sizeof(ulong) * 8; - WriteSteamId(msg.Message, ownerID); - msg.Message.BitPosition = prevBitPosition; - byte[] msgToSend = (byte[])msg.Message.Buffer.Clone(); - Array.Resize(ref msgToSend, msg.Message.LengthBytes); - ChildServerRelay.Write(msgToSend); + IWriteMessage msg = new WriteOnlyMessage(); + WriteSteamId(msg, unauthedMessage.Sender); + WriteSteamId(msg, ownerSteamId); + msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.Length); + ForwardToServerProcess(msg); } + remotePeer.UnauthedMessages.Clear(); } else { - DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication failed: " + status.ToString()); - return; + DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status)); } } @@ -130,67 +124,62 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (!remotePeers.Any(p => p.SteamID == steamId)) + if (remotePeers.None(p => p.SteamId.Value == steamId)) { - remotePeers.Add(new RemotePeer(steamId)); + remotePeers.Add(new RemotePeer(new SteamId(steamId))); } Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); //accept all connections, the server will figure things out later } - private void OnP2PData(ulong steamId, byte[] data, int dataLength, int channel) + private void OnP2PData(ulong steamId, IReadMessage inc) { if (!isActive) { return; } - RemotePeer remotePeer = remotePeers.Find(p => p.SteamID == steamId); - if (remotePeer == null || remotePeer.DisconnectTime != null) - { - return; - } + RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); + if (remotePeer == null) { return; } - IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, steamId); - WriteSteamId(outMsg, remotePeer.OwnerSteamID); - outMsg.Write(data, 1, dataLength - 1); + if (remotePeer.DisconnectTime != null) { return; } - DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; + var peerPacketHeaders = INetSerializableStruct.Read(inc); + + PacketHeader packetHeader = peerPacketHeaders.PacketHeader; - PacketHeader packetHeader = (PacketHeader)data[1]; - - if (!remotePeer.Authenticated & !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) + if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) { remotePeer.DisconnectTime = null; - IReadMessage authMsg = new ReadOnlyMessage(data, packetHeader.IsCompressed(), 2, dataLength - 2, null); - ConnectionInitialization initializationStep = (ConnectionInitialization)authMsg.ReadByte(); - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) + ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); + if (initialization == ConnectionInitialization.SteamTicketAndVersion) { remotePeer.Authenticating = true; - - authMsg.ReadString(); //skip name - authMsg.ReadInt32(); //skip owner key - authMsg.ReadUInt64(); //skip steamid - UInt16 ticketLength = authMsg.ReadUInt16(); - byte[] ticket = authMsg.ReadBytes(ticketLength); - Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); + var packet = INetSerializableStruct.Read(inc); + + packet.SteamAuthTicket.TryUnwrap(out byte[] ticket); + + Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); + DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); return; } } } + var steamUserId = new SteamId(steamId); if (remotePeer.Authenticating) { - remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage() { DeliveryMethod = deliveryMethod, Message = outMsg }); + remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(steamUserId, inc.Buffer)); } else { - byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); - Array.Resize(ref msgToSend, outMsg.LengthBytes); - ChildServerRelay.Write(msgToSend); + IWriteMessage outMsg = new WriteOnlyMessage(); + WriteSteamId(outMsg, steamUserId); + WriteSteamId(outMsg, remotePeer.OwnerSteamId.Fallback(steamUserId)); + outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes); + + ForwardToServerProcess(outMsg); } } @@ -198,11 +187,15 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true)) + if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false })) { - Close(); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; + msgBox.Buttons[0].OnClicked += (btn, obj) => + { + GameMain.MainMenuScreen.Select(); + return false; + }; return; } @@ -217,11 +210,12 @@ namespace Barotrauma.Networking for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } + var packet = Steamworks.SteamNetworking.ReadP2PPacket(); - if (packet.HasValue) + if (packet is { SteamId: var steamId, Data: var data }) { - OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); - receivedBytes += packet?.Data.Length ?? 0; + OnP2PData(steamId, new ReadWriteMessage(data, 0, data.Length * 8, false)); + receivedBytes += data.Length; } } @@ -240,177 +234,156 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - UInt64 recipientSteamId = ReadSteamId(inc); - DeliveryMethod deliveryMethod = (DeliveryMethod)inc.ReadByte(); + SteamId recipientSteamId = ReadSteamId(inc); - int p2pDataStart = inc.BytePosition; - - PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); + var peerPacketHeaders = INetSerializableStruct.Read(inc); if (recipientSteamId != selfSteamID) { - if (!packetHeader.IsServerMessage()) - { - DebugConsole.ThrowError("Received non-server message meant for remote peer"); - return; - } - - RemotePeer peer = remotePeers.Find(p => p.SteamID == recipientSteamId); - - if (peer == null) { return; } - - if (packetHeader.IsDisconnectMessage()) - { - DisconnectPeer(peer, inc.ReadString()); - return; - } - - Steamworks.P2PSend sendType; - switch (deliveryMethod) - { - case DeliveryMethod.Reliable: - case DeliveryMethod.ReliableOrdered: - //the documentation seems to suggest that the - //Reliable send type enforces packet order - sendType = Steamworks.P2PSend.Reliable; - break; - default: - sendType = Steamworks.P2PSend.Unreliable; - break; - } - - byte[] p2pData; - - if (packetHeader.IsConnectionInitializationStep()) - { - p2pData = new byte[inc.LengthBytes - p2pDataStart + 8]; - p2pData[0] = inc.Buffer[p2pDataStart]; - Lidgren.Network.NetBitWriter.WriteUInt64(SteamManager.CurrentLobbyID, 8 * 8, p2pData, 1 * 8); - Array.Copy(inc.Buffer, p2pDataStart+1, p2pData, 1 + 8, inc.LengthBytes - p2pDataStart - 1); - } - else - { - p2pData = new byte[inc.LengthBytes - p2pDataStart]; - Array.Copy(inc.Buffer, p2pDataStart, p2pData, 0, p2pData.Length); - - if (!packetHeader.IsHeartbeatMessage() && !packetHeader.IsDisconnectMessage()) - { - UInt16 length = Lidgren.Network.NetBitWriter.ReadUInt16(p2pData, 16, 8); - if (length > p2pData.Length - 2) - { - string errorMsg = $"Length written in message to send to client is larger than buffer size ({length} > {p2pData.Length - 2})"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "SteamP2POwnerPeerLengthValidationFail", - GameAnalyticsManager.ErrorSeverity.Error, - errorMsg); - } - } - } - - if (p2pData.Length + 4 >= MsgConstants.MTU) - { - DebugConsole.Log("WARNING: message length comes close to exceeding MTU, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); - sendType = Steamworks.P2PSend.Reliable; - } - - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); - sentBytes += p2pData.Length; - - if (!successSend) - { - if (sendType != Steamworks.P2PSend.Reliable) - { - DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); - sendType = Steamworks.P2PSend.Reliable; - successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); - sentBytes += p2pData.Length; - } - if (!successSend) - { - DebugConsole.AddWarning("Failed to send message to remote peer! (" + p2pData.Length.ToString() + " bytes)"); - } - } + HandleMessageForRemotePeer(peerPacketHeaders, recipientSteamId, inc); } else { - if (packetHeader.IsDisconnectMessage()) - { - DebugConsole.ThrowError("Received disconnect message from owned server"); - return; - } - if (!packetHeader.IsServerMessage()) - { - DebugConsole.ThrowError("Received non-server message from owned server"); - return; - } - if (packetHeader.IsHeartbeatMessage()) - { - return; //no timeout since we're using pipes, ignore this message - } - if (packetHeader.IsConnectionInitializationStep()) - { - IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, selfSteamID); - WriteSteamId(outMsg, selfSteamID); - outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); - outMsg.Write(Name); - - byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); - Array.Resize(ref msgToSend, outMsg.LengthBytes); - ChildServerRelay.Write(msgToSend); - return; - } - else - { - if (initializationStep != ConnectionInitialization.Success) - { - OnInitializationComplete?.Invoke(); - initializationStep = ConnectionInitialization.Success; - } - UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, ServerConnection); - OnMessageReceived?.Invoke(msg); - - return; - } + HandleMessageForOwner(peerPacketHeaders, inc); } } - private void DisconnectPeer(RemotePeer peer, string msg) + private static byte[] GetRemainingBytes(IReadMessage msg) { - if (!string.IsNullOrWhiteSpace(msg)) + return msg.Buffer[msg.BytePosition..msg.LengthBytes]; + } + + private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, SteamId recipientSteamId, IReadMessage inc) + { + var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders; + + if (!packetHeader.IsServerMessage()) { - if (peer.DisconnectTime == null) + DebugConsole.ThrowError("Received non-server message meant for remote peer"); + return; + } + + RemotePeer? peer = remotePeers.Find(p => p.SteamId == recipientSteamId); + if (peer is null) { return; } + + if (packetHeader.IsDisconnectMessage()) + { + var packet = INetSerializableStruct.Read(inc); + DisconnectPeer(peer, packet); + return; + } + + IWriteMessage outMsg = new WriteOnlyMessage(); + + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = packetHeader, + Initialization = initialization + }); + + if (packetHeader.IsConnectionInitializationStep()) + { + var initRelayPacket = new SteamP2PInitializationRelayPacket { - peer.DisconnectTime = Timing.TotalTime + 1.0; - } + LobbyID = SteamManager.CurrentLobbyID, + Message = new PeerPacketMessage + { + Buffer = GetRemainingBytes(inc) + } + }; - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)(PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage)); - outMsg.Write(msg); - - Steamworks.SteamNetworking.SendP2PPacket(peer.SteamID, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; + outMsg.WriteNetSerializableStruct(initRelayPacket); } else { - ClosePeerSession(peer); + byte[] userMessage = GetRemainingBytes(inc); + outMsg.WriteBytes(userMessage, 0, userMessage.Length); } + + ForwardToRemotePeer(deliveryMethod, recipientSteamId, outMsg); + } + + private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc) + { + var (_, packetHeader, _) = peerPacketHeaders; + + if (packetHeader.IsDisconnectMessage()) + { + DebugConsole.ThrowError("Received disconnect message from owned server"); + return; + } + + if (!packetHeader.IsServerMessage()) + { + DebugConsole.ThrowError("Received non-server message from owned server"); + return; + } + + if (packetHeader.IsHeartbeatMessage()) + { + return; //no timeout since we're using pipes, ignore this message + } + + if (packetHeader.IsConnectionInitializationStep()) + { + IWriteMessage outMsg = new WriteOnlyMessage(); + WriteSteamId(outMsg, selfSteamID); + WriteSteamId(outMsg, selfSteamID); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.SteamTicketAndVersion + }); + outMsg.WriteNetSerializableStruct(new SteamP2PInitializationOwnerPacket + { + OwnerName = GameMain.Client.Name + }); + ForwardToServerProcess(outMsg); + } + else + { + if (initializationStep != ConnectionInitialization.Success) + { + callbacks.OnInitializationComplete.Invoke(); + initializationStep = ConnectionInitialization.Success; + } + + PeerPacketMessage packet = INetSerializableStruct.Read(inc); + IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection); + callbacks.OnMessageReceived.Invoke(msg); + } + } + + private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket) + { + peer.DisconnectTime ??= Timing.TotalTime + 1.0; + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage + }); + outMsg.WriteNetSerializableStruct(peerDisconnectPacket); + + Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } private void ClosePeerSession(RemotePeer peer) { - Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamID); + Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamId.Value); remotePeers.Remove(peer); } public override void SendPassword(string password) { - return; //owner doesn't send passwords + //owner doesn't send passwords } - public override void Close(string msg = null, bool disableReconnect = false) + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } @@ -418,7 +391,7 @@ namespace Barotrauma.Networking for (int i = remotePeers.Count - 1; i >= 0; i--) { - DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + DisconnectPeer(remotePeers[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } Thread.Sleep(100); @@ -430,7 +403,7 @@ namespace Barotrauma.Networking ChildServerRelay.ClosePipes(); - OnDisconnect?.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); SteamManager.LeaveLobby(); Steamworks.SteamNetworking.ResetActions(); @@ -442,25 +415,62 @@ namespace Barotrauma.Networking if (!isActive) { return; } IWriteMessage msgToSend = new WriteOnlyMessage(); - byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); + byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); WriteSteamId(msgToSend, selfSteamID); WriteSteamId(msgToSend, selfSteamID); - msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); - msgToSend.Write((UInt16)length); - msgToSend.Write(msgData, 0, length); - - byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); - Array.Resize(ref bufToSend, msgToSend.LengthBytes); - ChildServerRelay.Write(bufToSend); + msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None + }); + msgToSend.WriteNetSerializableStruct(new PeerPacketMessage + { + Buffer = msgData + }); + ForwardToServerProcess(msgToSend); } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) { //not currently used by SteamP2POwnerPeer throw new NotImplementedException(); } + private static void ForwardToServerProcess(IWriteMessage msg) + { + byte[] bufToSend = new byte[msg.LengthBytes]; + msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan()); + ChildServerRelay.Write(bufToSend); + } + + private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, SteamId recipent, IWriteMessage outMsg) + { + byte[] buf = outMsg.PrepareForSending(compressPastThreshold: false, out _, out int length); + + if (length + 4 >= MsgConstants.MTU) + { + DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)"); + deliveryMethod = DeliveryMethod.Reliable; + } + + bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, deliveryMethod.ToSteam()); + sentBytes += length; + + if (successSend) { return; } + + if (deliveryMethod is DeliveryMethod.Unreliable) + { + DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)"); + successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, DeliveryMethod.Reliable.ToSteam()); + sentBytes += length; + } + + if (!successSend) + { + DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)"); + } + } + #if DEBUG public override void ForceTimeOut() { @@ -468,4 +478,4 @@ namespace Barotrauma.Networking } #endif } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 092f871d4..30a3b78a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -44,14 +44,19 @@ namespace Barotrauma.Networking if (!UseRespawnPrompt) { return; } if (CoroutineManager.IsCoroutineRunning(respawnPromptCoroutine) || GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "respawnquestionprompt")) { - return; + return; } respawnPromptCoroutine = CoroutineManager.Invoke(() => { if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } + + LocalizedString text = + TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)(SkillReductionOnDeath * 100)).ToString()) + + "\n\n" + TextManager.Get("respawnquestionprompt"); + var respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), + TextManager.Get("tutorial.tryagainheader"), text, new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) { UserData = "respawnquestionprompt" diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs deleted file mode 100644 index 797343808..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ /dev/null @@ -1,553 +0,0 @@ -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Barotrauma.Networking -{ - class ServerInfo - { - public string IP; - public string Port; - public string QueryPort; - - public Steamworks.Data.NetPingLocation? PingLocation; - public UInt64 LobbyID; - public UInt64 OwnerID; - public bool OwnerVerified; - - private string serverName; - public string ServerName - { - get { return serverName; } - set - { - serverName = value; - if (serverName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } - } - } - - public string ServerMessage; - public bool GameStarted; - public int PlayerCount; - public int MaxPlayers; - public bool HasPassword; - - public bool PingChecked; - public int Ping = -1; - - //null value means that the value isn't known (the server may be using - //an old version of the game that didn't report these values or the FetchRules query to Steam may not have finished yet) - public bool? UsingWhiteList; - public SelectionMode? ModeSelectionMode; - public SelectionMode? SubSelectionMode; - public bool? AllowSpectating; - public bool? VoipEnabled; - public bool? KarmaEnabled; - public bool? FriendlyFireEnabled; - public bool? AllowRespawn; - public YesNoMaybe? TraitorsEnabled; - public Identifier GameMode; - public PlayStyle? PlayStyle; - - public bool Recent; - public bool Favorite; - - public bool? RespondedToSteamQuery = null; - - public Steamworks.Friend? SteamFriend; - public Steamworks.SteamMatchmakingPingResponse MatchmakingPingResponse; - - public string GameVersion; - public List ContentPackageNames - { - get; - private set; - } = new List(); - public List ContentPackageHashes - { - get; - private set; - } = new List(); - public List ContentPackageWorkshopIds - { - get; - private set; - } = new List(); - - public void CreatePreviewWindow(GUIFrame frame) - { - if (frame == null) { return; } - - frame.ClearChildren(); - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) - { - ToolTip = ServerName, - CanBeFocused = false - }; - title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); - - GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), - "", null, "GUIServerListFavoriteTickBox") - { - Selected = Favorite, - ToolTip = TextManager.Get(Favorite ? "removefromfavorites" : "addtofavorites"), - OnSelected = (tickbox) => - { - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(this); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(this); - } - tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); - return true; - } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), - string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)) - { - CanBeFocused = false - }; - - bool hidePlaystyleBanner = !PlayStyle.HasValue; - if (!hidePlaystyleBanner) - { - PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; - Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; - float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; - var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform), - playStyleBannerSprite, null, true); - - var playStyleName = new GUITextBlock( - new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) - { RelativeOffset = new Vector2(0.0f, 0.06f) }, - TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), - TextManager.Get("servertag." + playStyle)), textColor: Color.White, - font: GUIStyle.SmallFont, textAlignment: Alignment.Center, - color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); - playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); - playStyleName.RectTransform.IsFixedSize = true; - } - - var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), - textAlignment: Alignment.TopLeft) - { - CanBeFocused = false - }; - serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); - - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) - { - Stretch = true - }; - // playstyle tags ----------------------------------------------------------------------------- - - var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f, - CanBeFocused = true - }; - - var playStyleTags = GetPlayStyleTags(); - foreach (string tag in playStyleTags) - { - if (!ServerListScreen.PlayStyleIcons.ContainsKey(tag)) { continue; } - - new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform), - ServerListScreen.PlayStyleIcons[tag], scaleToFit: true) - { - ToolTip = TextManager.Get("servertagdescription." + tag), - Color = ServerListScreen.PlayStyleIconColors[tag] - }; - } - - playStyleContainer.Recalculate(); - - // ----------------------------------------------------------------------------- - - float elementHeight = 0.075f; - - // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); - - var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; - var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true) - { - CanBeFocused = false - }; - serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; - msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; - - var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); - new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), - TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), - textAlignment: Alignment.Right); - - GUITextBlock playStyleText = null; - if (hidePlaystyleBanner && PlayStyle.HasValue) - { - PlayStyle playStyle = PlayStyle.Value; - playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); - new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); - } - - var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection")); - new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - - var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection")); - new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(!ModeSelectionMode.HasValue ? "Unknown" : ModeSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - - if (gameMode.TextSize.X + gameMode.GetChild().TextSize.X > gameMode.Rect.Width || - subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || - modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) - { - gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; - gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; - if (playStyleText != null) - { - playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; - } - } - - var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating")) - { - CanBeFocused = false - }; - if (!AllowSpectating.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowSpectating.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - allowSpectating.Selected = AllowSpectating.Value; - - var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) - { - CanBeFocused = false - }; - if (!AllowRespawn.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowRespawn.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - allowRespawn.Selected = AllowRespawn.Value; - - /*var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled")) - { - CanBeFocused = false - }; - if (!VoipEnabled.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), voipEnabledTickBox.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - voipEnabledTickBox.Selected = VoipEnabled.Value;*/ - - var usingWhiteList = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListUsingWhitelist")) - { - CanBeFocused = false - }; - if (!UsingWhiteList.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), usingWhiteList.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - usingWhiteList.Selected = UsingWhiteList.Value; - - content.RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(allowSpectating.TextBlock, allowRespawn.TextBlock, usingWhiteList.TextBlock); - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); - - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (component, o) => false - }; - if (ContentPackageNames.Count == 0) - { - new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else - { - for (int i = 0; i < ContentPackageNames.Count; i++) - { - var packageText = new GUITickBox( - new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) - { MinSize = new Point(0, 15) }, - ContentPackageNames[i]) - { - Enabled = false - }; - packageText.Box.Enabled = true; - packageText.TextBlock.Enabled = true; - if (i < ContentPackageHashes.Count) - { - if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i])) - { - packageText.TextColor = GUIStyle.Green; - packageText.Selected = true; - } - //workshop download link found - else if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) - { - packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); - } - else //no package or workshop download link found (TODO: update text to say that they could be downloaded through the server) - { - packageText.TextColor = GameMain.VanillaContent.NameMatches(ContentPackageNames[i]) ? GUIStyle.Red : GUIStyle.Yellow; - packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", - ("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i])); - } - } - } - } - - // ----------------------------------------------------------------------------- - - foreach (GUIComponent c in content.Children) - { - if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; } - } - } - - public IEnumerable GetPlayStyleTags() - { - List tags = new List(); - if (KarmaEnabled.HasValue) - { - tags.Add(KarmaEnabled.Value ? "karma.true" : "karma.false"); - } - if (TraitorsEnabled.HasValue) - { - tags.Add(TraitorsEnabled.Value == YesNoMaybe.Maybe ? - "traitors.maybe" : - (TraitorsEnabled.Value == YesNoMaybe.Yes ? "traitors.true" : "traitors.false")); - } - if (VoipEnabled.HasValue) - { - tags.Add(VoipEnabled.Value ? "voip.true" : "voip.false"); - } - if (FriendlyFireEnabled.HasValue) - { - tags.Add(FriendlyFireEnabled.Value ? "friendlyfire.true" : "friendlyfire.false"); - } - if (ContentPackageNames.Count > 0) - { - tags.Add(ContentPackageNames.Count > 1 || !GameMain.VanillaContent.NameMatches(ContentPackageNames[0]) ? "modded.true" : "modded.false"); - } - return tags; - } - - public static ServerInfo FromXElement(XElement element) - { - ServerInfo info = new ServerInfo() - { - ServerName = element.GetAttributeString("ServerName", ""), - ServerMessage = element.GetAttributeString("ServerMessage", ""), - IP = element.GetAttributeString("IP", ""), - Port = element.GetAttributeString("Port", ""), - QueryPort = element.GetAttributeString("QueryPort", ""), - OwnerID = element.GetAttributeSteamID("OwnerID",0) - }; - - info.RespondedToSteamQuery = null; - - info.GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty); - info.GameVersion = element.GetAttributeString("GameVersion", ""); - - int maxPlayersElement = element.GetAttributeInt("MaxPlayers", 0); - - if (maxPlayersElement > NetConfig.MaxPlayers) - { - /*DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red);*/ - maxPlayersElement = NetConfig.MaxPlayers; - } - - info.MaxPlayers = maxPlayersElement; - - if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; } - if (bool.TryParse(element.GetAttributeString("UsingWhiteList", ""), out bool whitelistTemp)) { info.UsingWhiteList = whitelistTemp; } - if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; } - if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; } - if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = modeSelectionTemp; } - if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; } - if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; } - if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; } - - info.HasPassword = element.GetAttributeBool("HasPassword", false); - - return info; - } - - public void QueryLiveInfo(Action onServerRulesReceived, Action onQueryDone) - { - if (!SteamManager.IsInitialized) { return; } - - if (int.TryParse(QueryPort, out int parsedPort) && IPAddress.TryParse(IP, out IPAddress parsedIP)) - { - if (MatchmakingPingResponse?.QueryActive ?? false) - { - MatchmakingPingResponse.Cancel(); - } - - MatchmakingPingResponse = new Steamworks.SteamMatchmakingPingResponse( - (server) => - { - ServerName = server.Name; - RespondedToSteamQuery = true; - PlayerCount = server.Players; - MaxPlayers = server.MaxPlayers; - HasPassword = server.Passworded; - PingChecked = true; - Ping = server.Ping; - LobbyID = 0; - TaskPool.Add("QueryServerRules (QueryLiveInfo)", server.QueryRulesAsync(), - (t) => - { - onQueryDone(this); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + ServerName); - return; - } - - t.TryGetResult(out Dictionary rules); - SteamManager.AssignServerRulesToServerInfo(rules, this); - - onServerRulesReceived(this); - }); - }, - () => - { - RespondedToSteamQuery = false; - }); - - MatchmakingPingResponse.HQueryPing(parsedIP, parsedPort); - } - else if (OwnerID != 0) - { - if (SteamFriend == null) - { - SteamFriend = new Steamworks.Friend(OwnerID); - } - if (LobbyID == 0) - { - TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(), - (t) => - { - onQueryDone(this); - if ((SteamFriend?.IsPlayingThisGame ?? false) && ((SteamFriend?.GameInfo?.Lobby?.Id ?? 0) != 0)) - { - LobbyID = SteamFriend?.GameInfo?.Lobby?.Id.Value ?? 0; - Steamworks.SteamMatchmaking.OnLobbyDataChanged += UpdateInfoFromSteamworksLobby; - SteamFriend?.GameInfo?.Lobby?.Refresh(); - } - else - { - RespondedToSteamQuery = false; - } - }); - } - else - { - onQueryDone(this); - } - } - } - - private void UpdateInfoFromSteamworksLobby(Steamworks.Data.Lobby lobby) - { - if (lobby.Id != LobbyID) { return; } - Steamworks.SteamMatchmaking.OnLobbyDataChanged -= UpdateInfoFromSteamworksLobby; - if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { return; } - bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword); - int.TryParse(lobby.GetData("playercount"), out int currPlayers); - int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers); - UInt64 ownerId = SteamManager.SteamIDStringToUInt64(lobby.GetData("lobbyowner")); - - if (OwnerID != ownerId) { return; } - - ServerName = lobby.GetData("name"); - IP = ""; - Port = ""; - QueryPort = ""; - PlayerCount = currPlayers; - MaxPlayers = maxPlayers; - HasPassword = hasPassword; - RespondedToSteamQuery = true; - LobbyID = lobby.Id; - OwnerID = ownerId; - PingChecked = false; - OwnerVerified = true; - - SteamManager.AssignLobbyDataToServerInfo(lobby, this); - } - - public XElement ToXElement() - { - if (OwnerID == 0 && string.IsNullOrEmpty(Port)) - { - return null; //can't save this one since it's not set up correctly - } - - XElement element = new XElement("ServerInfo"); - - element.SetAttributeValue("ServerName", ServerName); - element.SetAttributeValue("ServerMessage", ServerMessage); - if (OwnerID == 0) - { - element.SetAttributeValue("IP", IP); - element.SetAttributeValue("Port", Port); - element.SetAttributeValue("QueryPort", QueryPort); - } - else - { - element.SetAttributeValue("OwnerID", SteamManager.SteamIDUInt64ToString(OwnerID)); - } - - element.SetAttributeValue("GameMode", GameMode); - element.SetAttributeValue("GameVersion", GameVersion ?? ""); - element.SetAttributeValue("MaxPlayers", MaxPlayers); - if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); } - if (UsingWhiteList.HasValue) { element.SetAttributeValue("UsingWhiteList", UsingWhiteList.Value.ToString()); } - if (TraitorsEnabled.HasValue) { element.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.Value.ToString()); } - if (SubSelectionMode.HasValue) { element.SetAttributeValue("SubSelectionMode", SubSelectionMode.Value.ToString()); } - if (ModeSelectionMode.HasValue) { element.SetAttributeValue("ModeSelectionMode", ModeSelectionMode.Value.ToString()); } - if (VoipEnabled.HasValue) { element.SetAttributeValue("VoipEnabled", VoipEnabled.Value.ToString()); } - if (KarmaEnabled.HasValue) { element.SetAttributeValue("KarmaEnabled", KarmaEnabled.Value.ToString()); } - if (FriendlyFireEnabled.HasValue) { element.SetAttributeValue("FriendlyFireEnabled", FriendlyFireEnabled.Value.ToString()); } - element.SetAttributeValue("HasPassword", HasPassword.ToString()); - - return element; - } - - public override bool Equals(object obj) - { - return obj is ServerInfo other ? Equals(other) : base.Equals(obj); - } - - public bool Equals(ServerInfo other) - { - return - other.OwnerID == OwnerID && - (other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0) && - ((OwnerID == 0) ? (other.IP == IP && other.Port == Port) : true); - } - - public bool MatchesByEndpoint(ServerInfo other) - { - return OwnerID == other.OwnerID && (OwnerID != 0 ? true : (IP == other.IP && Port == other.Port)); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs new file mode 100644 index 000000000..1a1753091 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma +{ + abstract class FriendProvider + { + public abstract ServerListScreen.FriendInfo[] RetrieveFriends(); + public abstract void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize); + public abstract string GetUserName(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs new file mode 100644 index 000000000..9026de250 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs @@ -0,0 +1,67 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + class SteamFriendProvider : FriendProvider + { + private static ServerListScreen.FriendInfo FromSteamFriend(Steamworks.Friend steamFriend) + => new ServerListScreen.FriendInfo( + steamFriend.Name, + new SteamId(steamFriend.Id), + steamFriend.State switch + { + Steamworks.FriendState.Offline => ServerListScreen.FriendInfo.Status.Offline, + Steamworks.FriendState.Invisible => ServerListScreen.FriendInfo.Status.Offline, + _ when steamFriend.IsPlayingThisGame => ServerListScreen.FriendInfo.Status.PlayingBarotrauma, + _ when steamFriend.GameInfo is { GameID: var gameId } && gameId > 0 => ServerListScreen.FriendInfo.Status.PlayingAnotherGame, + _ => ServerListScreen.FriendInfo.Status.NotPlaying + }) + { + ServerName = steamFriend.GetRichPresence("servername"), + ConnectCommand = steamFriend.GetRichPresence("connect") is { } connectCmd + ? ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCmd)) + : Option.None() + }; + + public override ServerListScreen.FriendInfo[] RetrieveFriends() + => SteamManager.IsInitialized + ? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToArray() + : Array.Empty(); + + public override void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize) + { + if (!(friend.Id is SteamId steamId)) { return; } + + Func> avatarFunc = avatarSize switch + { + ServerListScreen.AvatarSize.Small => Steamworks.SteamFriends.GetSmallAvatarAsync, + ServerListScreen.AvatarSize.Medium => Steamworks.SteamFriends.GetMediumAvatarAsync, + ServerListScreen.AvatarSize.Large => Steamworks.SteamFriends.GetLargeAvatarAsync, + }; + TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(steamId.Value), task => + { + if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } + if (!(img is { } avatarImage)) { return; } + + if (friend.Avatar.TryUnwrap(out var prevAvatar)) + { + prevAvatar.Remove(); + } + + #warning TODO: create an avatar atlas? + var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); + avatarTexture.SetData(avatarImage.Data); + friend.Avatar = Option.Some(new Sprite(avatarTexture, null, null)); + }); + } + + public override string GetUserName() + => SteamManager.GetUsername(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs new file mode 100644 index 000000000..fc23c4df9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -0,0 +1,203 @@ +using Barotrauma.Steam; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; +using Socket = System.Net.Sockets.Socket; + +namespace Barotrauma.Networking +{ + static class PingUtils + { + private static readonly Dictionary activePings = new Dictionary(); + + private static bool steamPingInfoReady; + + public static void QueryPingData() + { + steamPingInfoReady = false; + if (SteamManager.IsInitialized) + { + TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task => + { + steamPingInfoReady = true; + }); + } + } + + public static void GetServerPing(ServerInfo serverInfo, Action onPingDiscovered) + { + if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } + + switch (serverInfo.Endpoint) + { + case LidgrenEndpoint { NetEndpoint: { Address: var address } }: + + GetIPAddressPing(serverInfo, address, onPingDiscovered); + break; + case SteamP2PEndpoint steamP2PEndpoint: + TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", + EstimateSteamLobbyPing(serverInfo), + t => + { + if (!t.TryGetResult(out Option ping)) { return; } + serverInfo.Ping = ping; + onPingDiscovered(serverInfo); + }); + break; + } + } + + private readonly ref struct LobbyDataChangedEventHandler + { + private readonly Action action; + + public LobbyDataChangedEventHandler(Action action) + { + this.action = action; + Steamworks.SteamMatchmaking.OnLobbyDataChanged += action; + } + + public void Dispose() + { + Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action; + } + } + + public static async Task GetSteamLobbyForUser(SteamId steamId) + { + var steamFriend = new Steamworks.Friend(steamId.Value); + await steamFriend.RequestInfoAsync(); + + var friendLobby = steamFriend.GameInfo?.Lobby; + if (!(friendLobby is { } lobby)) { return null; } + + bool waiting = true; + Lobby loadedLobby = default; + + void finishWaiting(Steamworks.Data.Lobby l) + { + loadedLobby = l; + waiting = false; + } + + using (new LobbyDataChangedEventHandler(finishWaiting)) + { + lobby.Refresh(); + + for (int i = 0;; i++) + { + if (!waiting) { break; } + if (i >= 100) { return null; } + } + } + + return loadedLobby; + } + + private static async Task> EstimateSteamLobbyPing(ServerInfo serverInfo) + { + if (!(serverInfo.Endpoint is SteamP2PEndpoint { SteamId: var ownerId })) { return Option.None(); } + while (!steamPingInfoReady) { await Task.Delay(50); } + + Lobby lobby; + + if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src)) + { + lobby = src.Lobby; + } + else + { + var friendLobby = await GetSteamLobbyForUser(ownerId); + if (friendLobby is null) { return Option.None(); } + lobby = friendLobby.Value; + } + + var pingLocation = NetPingLocation.TryParseFromString(lobby.GetData("pinglocation")); + + if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue) + { + int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value); + return ping >= 0 ? Option.Some(ping) : Option.None(); + } + else + { + return Option.None(); + } + } + + private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action onPingDiscovered) + { + if (IPAddress.IsLoopback(address)) + { + serverInfo.Ping = Option.Some(0); + onPingDiscovered(serverInfo); + } + else + { + lock (activePings) + { + if (activePings.ContainsKey(address)) { return; } + activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0); + } + serverInfo.Ping = Option.None(); + TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), + rtt => + { + if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option.None(); } + onPingDiscovered(serverInfo); + lock (activePings) + { + activePings.Remove(address); + } + }); + } + } + + private static async Task> PingServerAsync(IPAddress ipAddress, int timeOut) + { + await Task.Yield(); + bool shouldGo = false; + while (!shouldGo) + { + lock (activePings) + { + shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; + } + await Task.Delay(25); + } + + if (ipAddress == null) { return Option.None(); } + + //don't attempt to ping if the address is IPv6 and it's not supported + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } + + Ping ping = new Ping(); + byte[] buffer = new byte[32]; + try + { + PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true)); + + return pingReply.Status switch + { + IPStatus.Success => Option.Some((int)pingReply.RoundtripTime), + _ => Option.None(), + }; + } + catch (Exception ex) + { + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); +#if DEBUG + DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); +#endif + + return Option.None(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs new file mode 100644 index 000000000..adbf863df --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -0,0 +1,509 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Steam; + +namespace Barotrauma.Networking +{ + sealed class ServerInfo : ISerializableEntity + { + public abstract class DataSource + { + public static Option Parse(XElement element) + => ReflectionUtils.ParseDerived(element); + public abstract void Write(XElement element); + } + + public Endpoint Endpoint { get; private set; } + + public Option MetadataSource = Option.None(); + + [Serialize("", IsPropertySaveable.Yes)] + public string ServerName { get; set; } = ""; + + [Serialize("", IsPropertySaveable.Yes)] + public string ServerMessage { get; set; } = ""; + + public int PlayerCount { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int MaxPlayers { get; set; } + + public bool GameStarted { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool HasPassword { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier GameMode { get; set; } + + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] + public SelectionMode ModeSelectionMode { get; set; } + + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] + public SelectionMode SubSelectionMode { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowSpectating { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool VoipEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool KarmaEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool FriendlyFireEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowRespawn { get; set; } + + [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] + public YesNoMaybe TraitorsEnabled { get; set; } + + [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] + public PlayStyle PlayStyle { get; set; } + + public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); + + public Option Ping = Option.None(); + + public bool Checked = false; + + public readonly struct ContentPackageInfo + { + public readonly string Name; + public readonly string Hash; + public readonly Option Id; + + public ContentPackageInfo(string name, string hash, Option id) + { + Name = name; + Hash = hash; + Id = id; + } + + public ContentPackageInfo(ContentPackage pkg) + { + Name = pkg.Name; + Hash = pkg.Hash.StringRepresentation; + Id = pkg.UgcId; + } + } + + public ImmutableArray ContentPackages; + + public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name)); + + public ServerInfo(Endpoint endpoint) + { + SerializableProperties = SerializableProperty.GetProperties(this); + Endpoint = endpoint; + ContentPackages = ImmutableArray.Empty; + } + + public static ServerInfo FromServerConnection(NetworkConnection connection, ServerSettings serverSettings) + { + var serverInfo = new ServerInfo(connection.Endpoint) + { + GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty, + GameStarted = Screen.Selected != GameMain.NetLobbyScreen, + GameVersion = GameMain.Version, + PlayerCount = GameMain.Client.ConnectedClients.Count, + ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ContentPackageInfo(p)).ToImmutableArray(), + Ping = GameMain.Client.Ping, + + // ------------------------------------- + // Settings that cannot be copied via + // SerializableProperty because they do + // not implement the attribute + ServerName = serverSettings.ServerName, + ServerMessage = serverSettings.ServerMessageText, + // ------------------------------------- + // Settings that cannot be copied via + // SerializableProperty due to name mismatch + HasPassword = serverSettings.HasPassword, + VoipEnabled = serverSettings.VoiceChatEnabled, + FriendlyFireEnabled = serverSettings.AllowFriendlyFire, + // ------------------------------------- + + Checked = true + }; + + var serverInfoSerializableProperties + = SerializableProperty.GetProperties(serverInfo); + var serverSettingsSerializableProperties + = SerializableProperty.GetProperties(serverSettings); + + var intersection = serverInfoSerializableProperties.Keys + .Where(serverSettingsSerializableProperties.ContainsKey); + + foreach (var key in intersection) + { + var propToGet = serverSettingsSerializableProperties[key]; + var propToSet = serverInfoSerializableProperties[key]; + if (!propToGet.PropertyInfo.CanRead) { continue; } + if (!propToSet.PropertyInfo.CanWrite) { continue; } + propToSet.SetValue( + serverInfo, + propToGet.GetValue(serverSettings)); + } + + return serverInfo; + } + + public void CreatePreviewWindow(GUIFrame frame) + { + frame.ClearChildren(); + + var serverListScreen = GameMain.ServerListScreen; + + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) + { + ToolTip = ServerName, + CanBeFocused = false + }; + title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); + + bool isFavorite = serverListScreen.IsFavorite(this); + + static LocalizedString favoriteTickBoxToolTip(bool isFavorite) + => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); + + GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), + "", null, "GUIServerListFavoriteTickBox") + { + UserData = this, + Selected = isFavorite, + ToolTip = favoriteTickBoxToolTip(isFavorite), + OnSelected = tickbox => + { + ServerInfo info = (ServerInfo)tickbox.UserData; + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); + return true; + } + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), + GameVersion == new Version(0, 0, 0, 0) ? TextManager.Get("Unknown") : GameVersion.ToString())) + { + CanBeFocused = false + }; + + PlayStyle playStyle = PlayStyle; + Sprite? playStyleBannerSprite = GUIStyle.GetComponentStyle($"PlayStyleBanner.{playStyle}")?.GetSprite(GUIComponent.ComponentState.None); + + GUIComponent playStyleBanner; + Color playStyleBannerColor; + if (playStyleBannerSprite != null) + { + float playStyleBannerAspectRatio = (float)playStyleBannerSprite.SourceRect.Width / (float)playStyleBannerSprite.SourceRect.Height; + playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), frame.RectTransform, scaleBasis: ScaleBasis.BothWidth), + playStyleBannerSprite, null, true); + playStyleBannerColor = playStyleBannerSprite.SourceElement.GetAttributeColor("bannercolor", Color.Black); + } + else + { + playStyleBanner = new GUIFrame(new RectTransform((1.0f, 0.2f), frame.RectTransform), style: null) + { + Color = Color.Black, + DisabledColor = Color.Black, + OutlineColor = Color.Black, + PressedColor = Color.Black, + SelectedColor = Color.Black, + HoverColor = Color.Black + }; + playStyleBannerColor = Color.Black; + } + + var playStyleName = new GUITextBlock( + new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) + { RelativeOffset = new Vector2(0.0f, 0.06f) }, + TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), + TextManager.Get($"servertag.{playStyle}")), textColor: Color.White, + font: GUIStyle.SmallFont, textAlignment: Alignment.Center, + color: playStyleBannerColor, style: "GUISlopedHeader"); + playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); + playStyleName.RectTransform.IsFixedSize = true; + + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + Endpoint?.ServerTypeString ?? string.Empty, + textAlignment: Alignment.TopLeft) + { + CanBeFocused = false + }; + serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); + + var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) + { + Stretch = true + }; + // playstyle tags ----------------------------------------------------------------------------- + + var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f, + CanBeFocused = true + }; + + var playStyleTags = GetPlayStyleTags(); + foreach (var tag in playStyleTags) + { + var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}") + ?.GetSprite(GUIComponent.ComponentState.None); + if (playStyleIcon is null) { continue; } + + new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform), + playStyleIcon, scaleToFit: true) + { + ToolTip = TextManager.Get($"servertagdescription.{tag}"), + Color = Color.White + }; + } + + playStyleContainer.Recalculate(); + + // ----------------------------------------------------------------------------- + + float elementHeight = 0.075f; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); + + var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; + var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage ?? string.Empty, font: GUIStyle.SmallFont, wrap: true) + { + CanBeFocused = false + }; + serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; + msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); + new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), + TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), + textAlignment: Alignment.Right); + + GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); + new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); + + var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection")); + new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(SubSelectionMode.ToString()), textAlignment: Alignment.Right); + + var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection")); + new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(ModeSelectionMode.ToString()), textAlignment: Alignment.Right); + + if (gameMode.TextSize.X + gameMode.GetChild().TextSize.X > gameMode.Rect.Width || + subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || + modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) + { + gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; + gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; + playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; + } + + var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating")) + { + CanBeFocused = false + }; + allowSpectating.Selected = AllowSpectating; + + var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) + { + CanBeFocused = false + }; + allowRespawn.Selected = AllowRespawn; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) + { + ScrollBarVisible = true, + OnSelected = (component, o) => false + }; + if (ContentPackages.Length == 0) + { + new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + } + else + { + foreach (var package in ContentPackages) + { + var packageText = new GUITickBox( + new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) + { MinSize = new Point(0, 15) }, + package.Name) + { + CanBeFocused = false + }; + if (!string.IsNullOrEmpty(package.Hash)) + { + if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == package.Hash)) + { + packageText.TextColor = GUIStyle.Green; + packageText.Selected = true; + } + //workshop download link found + else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + { + packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); + } + else //no package or workshop download link found + { + packageText.TextColor = GameMain.VanillaContent.NameMatches(package.Name) ? GUIStyle.Red : GUIStyle.Yellow; + packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", + ("[contentpackage]", package.Name), ("[hash]", package.Hash)); + } + } + } + } + + // ----------------------------------------------------------------------------- + + foreach (GUIComponent c in content.Children) + { + if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; } + } + } + + public IEnumerable GetPlayStyleTags() + { + yield return $"Karma.{KarmaEnabled}".ToIdentifier(); + yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); + yield return $"VoIP.{VoipEnabled}".ToIdentifier(); + yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier(); + yield return $"Modded.{ContentPackages.Any()}".ToIdentifier(); + } + + public void UpdateInfo(Func valueGetter) + { + ServerMessage = valueGetter("message") ?? ""; + if (Version.TryParse(valueGetter("version"), out var version)) + { + GameVersion = version; + } + if (int.TryParse(valueGetter("playercount"), out int playerCount)) { PlayerCount = playerCount; } + if (int.TryParse(valueGetter("maxplayernum"), out int maxPlayers)) { MaxPlayers = maxPlayers; } + if (Enum.TryParse(valueGetter("modeselectionmode"), out SelectionMode modeSelectionMode)) { ModeSelectionMode = modeSelectionMode; } + if (Enum.TryParse(valueGetter("subselectionmode"), out SelectionMode subSelectionMode)) { SubSelectionMode = subSelectionMode; } + + HasPassword = getBool("haspassword"); + GameStarted = getBool("gamestarted"); + KarmaEnabled = getBool("karmaenabled"); + FriendlyFireEnabled = getBool("friendlyfireenabled"); + AllowSpectating = getBool("allowspectating"); + AllowRespawn = getBool("allowrespawn"); + VoipEnabled = getBool("voicechatenabled"); + + GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; + if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } + if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + + ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); + + bool getBool(string key) + { + string? data = valueGetter(key); + return bool.TryParse(data, out var result) && result; + } + } + + private static ContentPackageInfo[] ExtractContentPackageInfo(Func valueGetter) + { + string? joinedNames = valueGetter("contentpackage"); + string? joinedHashes = valueGetter("contentpackagehash"); + string? joinedWorkshopIds = valueGetter("contentpackageid"); + + string[] contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty() : joinedNames.Split(','); + string[] contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty() : joinedHashes.Split(','); + #warning TODO: genericize + ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); + + if (contentPackageNames.Length != contentPackageHashes.Length + || contentPackageHashes.Length != contentPackageIds.Length) + { + return Array.Empty(); + } + + return contentPackageNames + .Zip(contentPackageHashes, (name, hash) => (name, hash)) + .Zip(contentPackageIds, (t1, id) => + new ContentPackageInfo( + t1.name, + t1.hash, + Option.Some(new SteamWorkshopId(id)))) + .ToArray(); + } + + public static Option FromXElement(XElement element) + { + string endpointStr + = element.GetAttributeString("Endpoint", null) + ?? element.GetAttributeString("OwnerID", null) + ?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}"; + + if (!Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) { return Option.None(); } + + var gameVersionStr = element.GetAttributeString("GameVersion", ""); + if (!Version.TryParse(gameVersionStr, out var gameVersion)) { gameVersion = GameMain.Version; } + var info = new ServerInfo(endpoint) + { + GameVersion = gameVersion + }; + SerializableProperty.DeserializeProperties(info, element); + + info.MetadataSource = DataSource.Parse(element); + + return Option.Some(info); + } + + public XElement ToXElement() + { + XElement element = new XElement(GetType().Name); + + element.SetAttributeValue("Endpoint", Endpoint.ToString()); + element.SetAttributeValue("GameVersion", GameVersion.ToString()); + + SerializableProperty.SerializeProperties(this, element, saveIfDefault: true); + + if (MetadataSource.TryUnwrap(out var dataSource)) + { + dataSource.Write(element); + } + + return element; + } + + public override bool Equals(object? obj) + { + return obj is ServerInfo other && Equals(other); + } + + public bool Equals(ServerInfo other) + => other.Endpoint == Endpoint; + + public override int GetHashCode() => Endpoint.GetHashCode(); + + string ISerializableEntity.Name => "ServerInfo"; + public Dictionary SerializableProperties { get; } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs new file mode 100644 index 000000000..205d4f034 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs @@ -0,0 +1,35 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma +{ + class CompositeServerProvider : ServerProvider + { + private readonly ImmutableArray providers; + + public CompositeServerProvider(params ServerProvider[] providers) + { + this.providers = providers.ToImmutableArray(); + } + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + int providersFinished = 0; + void ackFinishedProvider() + { + providersFinished++; + if (providersFinished == providers.Length) + { + onQueryCompleted(); + } + } + providers.ForEach(p => p.RetrieveServers(onServerDataReceived, ackFinishedProvider)); + } + + public override void Cancel() + => providers.ForEach(p => p.Cancel()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs new file mode 100644 index 000000000..8664c58ed --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs @@ -0,0 +1,17 @@ +#nullable enable +using System; +using Barotrauma.Networking; + +namespace Barotrauma +{ + abstract class ServerProvider + { + public void RetrieveServers(Action onServerDataReceived, Action onQueryCompleted) + { + Cancel(); + RetrieveServersImpl(onServerDataReceived, onQueryCompleted); + } + protected abstract void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted); + public abstract void Cancel(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs new file mode 100644 index 000000000..ff9079caf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -0,0 +1,160 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma +{ + sealed class SteamDedicatedServerProvider : ServerProvider + { + public class DataSource : ServerInfo.DataSource + { + public readonly UInt16 QueryPort; + + public DataSource(UInt16 queryPort) + { + QueryPort = queryPort; + } + + /// Method is invoked via reflection, + /// see + public new static Option Parse(XElement element) + => element.TryGetAttributeInt("QueryPort", out var result) + ? result switch + { + var invalidPort when invalidPort <= 0 || invalidPort > UInt16.MaxValue => Option.None(), + var queryPort => Option.Some(new DataSource((UInt16)queryPort)) + } + : Option.None(); + + public override void Write(XElement element) => element.SetAttributeValue("QueryPort", QueryPort); + } + + private static Option InfoFromListEntry(Steamworks.Data.ServerInfo entry) => + entry.Name.IsNullOrEmpty() + ? Option.None() + : Option.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) + { + ServerName = entry.Name, + HasPassword = entry.Passworded, + PlayerCount = entry.Players, + MaxPlayers = entry.MaxPlayers, + MetadataSource = Option.Some(new DataSource((UInt16)entry.QueryPort)) + }); + + private static void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + { + TaskPool.Add($"QueryServerRules (GetServers, {entry.Name}, {entry.Address})", entry.QueryRulesAsync(), + t => + { + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}"); + return; + } + + if (!t.TryGetResult(out Dictionary rules)) { return; } + if (rules is null) { return; } + if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } + serverInfo.UpdateInfo(key => + { + if (rules.TryGetValue(key, out var val)) { return val; } + return null; + }); + serverInfo.Checked = true; //rules != null; + + onServerDataReceived(serverInfo); + }); + } + + private static void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + { + //TODO: do we still want to list unresponsive servers? + if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } + onServerDataReceived(serverInfo); + } + + private Steamworks.ServerList.Internet? serverQuery; + private CoroutineHandle? queryCoroutine; + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + if (!SteamManager.IsInitialized) + { + onQueryCompleted(); + return; + } + + // All lambdas in here must only capture this call's + // query, not the provider's latest query + var selfServerQuery = new Steamworks.ServerList.Internet(); + serverQuery = selfServerQuery; + + ConcurrentQueue responsiveServers = + new ConcurrentQueue(); + ConcurrentQueue unresponsiveServers = + new ConcurrentQueue(); + + selfServerQuery.OnResponsiveServer = responsiveServers.Enqueue; + selfServerQuery.OnUnresponsiveServer = unresponsiveServers.Enqueue; + + void dequeue(int? limit = null) + { + for (int i = 0; (!limit.HasValue || i < limit) && responsiveServers.TryDequeue(out var serverInfo); i++) + { + HandleResponsiveServer(serverInfo, onServerDataReceived); + } + + for (int i = 0; (!limit.HasValue || i < limit) && unresponsiveServers.TryDequeue(out var serverInfo); i++) + { + HandleUnresponsiveServer(serverInfo, onServerDataReceived); + } + } + + IEnumerable dequeueCoroutine() + { + while (true) + { + dequeue(limit: 20); + yield return new WaitForSeconds(0.1f, ignorePause: true); + } + } + var selfQueryCoroutine = CoroutineManager.StartCoroutine(dequeueCoroutine(), + $"{nameof(SteamDedicatedServerProvider)}.{nameof(RetrieveServers)}.{nameof(dequeueCoroutine)}"); + queryCoroutine = selfQueryCoroutine; + + TaskPool.Add("RunServerQuery", selfServerQuery.RunQueryAsync(timeoutSeconds: 30f), + t => + { + try + { + // Clear the callbacks because it's too late now, we want to get this over with + selfServerQuery.OnResponsiveServer = null; + selfServerQuery.OnUnresponsiveServer = null; + + CoroutineManager.StopCoroutines(selfQueryCoroutine); + dequeue(); + + if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); } + + selfServerQuery.Dispose(); + } + finally + { + onQueryCompleted(); + } + }); + } + + public override void Cancel() + { + if (queryCoroutine != null) { CoroutineManager.StopCoroutines(queryCoroutine); } + serverQuery?.Dispose(); + serverQuery = null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs new file mode 100644 index 000000000..fe80749e5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma +{ + sealed class SteamP2PServerProvider : ServerProvider + { + public class DataSource : ServerInfo.DataSource + { + public readonly Steamworks.Data.Lobby Lobby; + + public override void Write(XElement element) { /* do nothing */ } + + public DataSource(Steamworks.Data.Lobby lobby) + { + Lobby = lobby; + } + } + + private object? queryRef = null; + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + if (!SteamManager.IsInitialized) + { + onQueryCompleted(); + return; + } + + // All lambdas and local methods in here must only capture + // this call's query, not the provider's latest query + var selfQueryRef = new object(); + queryRef = selfQueryRef; + + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() + .FilterDistanceWorldwide() + .WithMaxResults(50); + // Steamworks is unable to retrieve more than 50 lobbies per request + // (see https://partner.steamgames.com/doc/features/multiplayer/matchmaking#3) + // To work around this, we'll make up to 10 requests, asking to ignore + // all previous results in each subsequent request. + #warning TODO: do something less horrible here? + + int requestCount = 0; + HashSet retrieved = new HashSet(); + + void startQuery() + { + if (requestCount >= 10) { return; } + requestCount++; + TaskPool.Add($"LobbyQuery.RequestAsync ({requestCount})", lobbyQuery.RequestAsync(), onRequestComplete); + } + + void onRequestComplete(Task t) + { + // If queryRef != selfQueryRef, this query was cancelled + if (!ReferenceEquals(selfQueryRef, queryRef)) { return; } + + if (!t.TryGetResult(out Steamworks.Data.Lobby[] lobbies) + || lobbies is null + || lobbies.Length == 0) + { + onQueryCompleted(); + return; + } + + foreach (var lobby in lobbies) + { + string lobbyOwnerStr = lobby.GetData("lobbyowner"); + lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); + + string serverName = lobby.GetData("name"); + if (string.IsNullOrEmpty(serverName)) { continue; } + + var ownerId = SteamId.Parse(lobbyOwnerStr); + if (!ownerId.TryUnwrap(out var lobbyOwnerId)) { continue; } + + if (retrieved.Contains(lobbyOwnerId)) { continue; } + retrieved.Add(lobbyOwnerId); + + var serverInfo = new ServerInfo(new SteamP2PEndpoint(lobbyOwnerId)) + { + ServerName = serverName, + MetadataSource = Option.Some(new DataSource(lobby)) + }; + serverInfo.UpdateInfo(key => lobby.GetData(key)); + serverInfo.Checked = true; + + onServerDataReceived(serverInfo); + } + startQuery(); + } + + startQuery(); + } + + public override void Cancel() + { + queryRef = null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 19a9ba612..e0476a625 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Xna.Framework.Graphics; -using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -196,11 +196,7 @@ namespace Barotrauma.Networking { foreach (var data in richString.RichTextData.Value) { - if (!UInt64.TryParse(data.Metadata, out ulong id)) { return; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) - ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); + Client client = data.ExtractClient(); if (client != null && client.Karma < 40.0f) { textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), @@ -258,11 +254,8 @@ namespace Barotrauma.Networking foreach (GUIComponent child in listBox.Content.Children) { - var textBlock = child as GUITextBlock; - if (textBlock == null) continue; - + if (!(child is GUITextBlock textBlock)) { continue; } child.Visible = true; - if (msgTypeHidden[(int)((LogMessage)child.UserData).Type]) { child.Visible = false; @@ -287,10 +280,10 @@ namespace Barotrauma.Networking listBox.Content.RectTransform.ReverseChildren(); } - public bool ClearFilter(GUIComponent button, object obj) + public bool ClearFilter(GUIComponent button, object _) { var searchBox = button.UserData as GUITextBox; - if (searchBox != null) searchBox.Text = ""; + if (searchBox != null) { searchBox.Text = ""; } msgFilter = ""; FilterMessages(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index f03d7b099..10829d4fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -121,13 +121,10 @@ namespace Barotrauma.Networking ReadMonsterEnabled(incMsg); BanList.ClientAdminRead(incMsg); - Whitelist.ClientAdminRead(incMsg); } public void ClientRead(IReadMessage incMsg) { - cachedServerListInfo = null; - NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); if (requiredFlags.HasFlag(NetFlags.Name)) @@ -147,7 +144,6 @@ namespace Barotrauma.Networking AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); - GameMain.NetworkMember.TickRate = TickRate; if (requiredFlags.HasFlag(NetFlags.Properties)) { @@ -183,9 +179,9 @@ namespace Barotrauma.Networking IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)ClientPacketHeader.SERVER_SETTINGS); + outMsg.WriteByte((byte)ClientPacketHeader.SERVER_SETTINGS); - outMsg.Write((byte)dataToSend); + outMsg.WriteByte((byte)dataToSend); if (dataToSend.HasFlag(NetFlags.Name)) { @@ -193,7 +189,7 @@ namespace Barotrauma.Networking { ServerName = GameMain.NetLobbyScreen.ServerName.Text; } - outMsg.Write(ServerName); + outMsg.WriteString(ServerName); } if (dataToSend.HasFlag(NetFlags.Message)) @@ -202,7 +198,7 @@ namespace Barotrauma.Networking { ServerMessageText = GameMain.NetLobbyScreen.ServerMessage.Text; } - outMsg.Write(ServerMessageText); + outMsg.WriteString(ServerMessageText); } if (dataToSend.HasFlag(NetFlags.Properties)) @@ -214,18 +210,17 @@ namespace Barotrauma.Networking UInt32 count = (UInt32)changedProperties.Count(); bool changedMonsterSettings = tempMonsterEnabled != null && tempMonsterEnabled.Any(p => p.Value != MonsterEnabled[p.Key]); - outMsg.Write(count); + outMsg.WriteUInt32(count); foreach (KeyValuePair prop in changedProperties) { DebugConsole.NewMessage(prop.Value.Name.Value, Color.Lime); - outMsg.Write(prop.Key); + outMsg.WriteUInt32(prop.Key); prop.Value.Write(outMsg, prop.Value.GUIComponentValue); } - outMsg.Write(changedMonsterSettings); outMsg.WritePadBits(); + outMsg.WriteBoolean(changedMonsterSettings); outMsg.WritePadBits(); if (changedMonsterSettings) WriteMonsterEnabled(outMsg, tempMonsterEnabled); BanList.ClientAdminWrite(outMsg); - Whitelist.ClientAdminWrite(outMsg); } if (dataToSend.HasFlag(NetFlags.HiddenSubs)) @@ -237,23 +232,24 @@ namespace Barotrauma.Networking { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); - outMsg.Write((byte)(traitorSetting + 1)); - outMsg.Write((byte)(botCount + 1)); - outMsg.Write((byte)(botSpawnMode + 1)); + outMsg.WriteByte((byte)(traitorSetting + 1)); + outMsg.WriteByte((byte)(botCount + 1)); + outMsg.WriteByte((byte)(botSpawnMode + 1)); - outMsg.Write(levelDifficulty ?? -1000.0f); + outMsg.WriteSingle(levelDifficulty ?? -1000.0f); - outMsg.Write(useRespawnShuttle ?? UseRespawnShuttle); + outMsg.WriteBoolean(useRespawnShuttle != null); + outMsg.WriteBoolean(useRespawnShuttle ?? false); - outMsg.Write(autoRestart != null); - outMsg.Write(autoRestart ?? false); + outMsg.WriteBoolean(autoRestart != null); + outMsg.WriteBoolean(autoRestart ?? false); outMsg.WritePadBits(); } if (dataToSend.HasFlag(NetFlags.LevelSeed)) { - outMsg.Write(GameMain.NetLobbyScreen.SeedBox.Text); + outMsg.WriteString(GameMain.NetLobbyScreen.SeedBox.Text); } GameMain.Client.ClientPeer.Send(outMsg, DeliveryMethod.Reliable); @@ -273,8 +269,7 @@ namespace Barotrauma.Networking General, Rounds, Antigriefing, - Banlist, - Whitelist + Banlist } private NetPropertyData GetPropertyData(string name) @@ -949,13 +944,6 @@ namespace Barotrauma.Networking //-------------------------------------------------------------------------------- BanList.CreateBanFrame(settingsTabs[(int)SettingsTab.Banlist]); - - //-------------------------------------------------------------------------------- - // whitelist - //-------------------------------------------------------------------------------- - - Whitelist.CreateWhiteListFrame(settingsTabs[(int)SettingsTab.Whitelist]); - Whitelist.localEnabled = Whitelist.Enabled; } private void CreateLabeledSlider(GUIComponent parent, string labelTag, out GUIScrollBar slider, out GUITextBlock label) @@ -1067,15 +1055,7 @@ namespace Barotrauma.Networking } settingsFrame = null; } - return false; } - - private ServerInfo cachedServerListInfo = null; - public ServerInfo GetServerListInfo() - { - cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this); - return cachedServerListInfo; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 67eeca690..bfbba0c7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using OpenAL; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -22,13 +21,13 @@ namespace Barotrauma.Networking public static IReadOnlyList CaptureDeviceNames => Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); - private IntPtr captureDevice; + private readonly IntPtr captureDevice; private Thread captureThread; private bool capturing; - private OpusEncoder encoder; + private readonly OpusEncoder encoder; public double LastdB { @@ -53,7 +52,7 @@ namespace Barotrauma.Networking { get { - return GameMain.Client?.ID ?? 0; + return GameMain.Client?.SessionId ?? 0; } protected set { @@ -82,7 +81,7 @@ namespace Barotrauma.Networking } } - private VoipCapture(string deviceName) : base(GameMain.Client?.ID ?? 0, true, false) + private VoipCapture(string deviceName) : base(GameMain.Client?.SessionId ?? 0, true, false) { Disconnected = false; @@ -171,8 +170,8 @@ namespace Barotrauma.Networking } IntPtr nativeBuffer; - short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; - short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; bool prevCaptured = true; int captureTimer; @@ -227,15 +226,20 @@ namespace Barotrauma.Networking bool allowEnqueue = overrideSound != null; if (GameMain.WindowActive && SettingsMenu.Instance is null) { - bool pttDown = PlayerInput.KeyDown(InputType.Voice) && GUI.KeyboardDispatcher.Subscriber == null; - if (GameMain.LuaCs.Game.ForceVoice != null) { pttDown = GameMain.LuaCs.Game.ForceVoice.Value; } - if (pttDown || captureTimer <= 0) - { - ForceLocal = GameMain.ActiveChatMode == ChatMode.Local; - if (GameMain.LuaCs.Game.ForceLocalVoice != null) { ForceLocal = GameMain.LuaCs.Game.ForceLocalVoice.Value; } - } + bool usingLocalMode = PlayerInput.KeyDown(InputType.LocalVoice); + bool usingRadioMode = PlayerInput.KeyDown(InputType.RadioVoice); if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { + bool pttDown = (usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; + if (pttDown) + { + ForceLocal = usingLocalMode; + } + //in Activity mode, we default to the active mode UNLESS a specific ptt key is held + else + { + ForceLocal = GameMain.ActiveChatMode == ChatMode.Local; + } if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold) { allowEnqueue = true; @@ -243,6 +247,13 @@ namespace Barotrauma.Networking } else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.PushToTalk) { + //in push-to-talk mode, InputType.Voice uses the active chat mode + bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice); + bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; + if (pttDown || captureTimer <= 0) + { + ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode; + } if (pttDown) { allowEnqueue = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index eabc6004a..934820e0d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -72,8 +72,8 @@ namespace Barotrauma.Networking { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.VOICE); - msg.Write((byte)VoipCapture.Instance.QueueID); + msg.WriteByte((byte)ClientPacketHeader.VOICE); + msg.WriteByte((byte)VoipCapture.Instance.QueueID); VoipCapture.Instance.Write(msg); netClient.Send(msg, DeliveryMethod.Unreliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 90382e7c9..f07d6f7a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -2,6 +2,8 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; +using System; namespace Barotrauma { @@ -54,7 +56,7 @@ namespace Barotrauma voteCountMax[voteType] = value; } - public void UpdateVoteTexts(List clients, VoteType voteType) + public void UpdateVoteTexts(IEnumerable clients, VoteType voteType) { switch (voteType) { @@ -92,7 +94,7 @@ namespace Barotrauma private void SetVoteText(GUIListBox listBox, object userData, int votes) { - if (userData == null) return; + if (userData == null) { return; } foreach (GUIComponent comp in listBox.Content.Children) { if (comp.UserData != userData) { continue; } @@ -110,37 +112,54 @@ namespace Barotrauma } } + public void ResetVotes(IEnumerable connectedClients) + { + foreach (Client client in connectedClients) + { + client.ResetVotes(); + } + + foreach (VoteType voteType in Enum.GetValues(typeof(VoteType))) + { + SetVoteCountYes(voteType, 0); + SetVoteCountNo(voteType, 0); + SetVoteCountMax(voteType, 0); + } + UpdateVoteTexts(connectedClients, VoteType.Mode); + UpdateVoteTexts(connectedClients, VoteType.Sub); + } + public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) { - msg.Write((byte)voteType); + msg.WriteByte((byte)voteType); switch (voteType) { case VoteType.Sub: if (!(data is SubmarineInfo sub)) { return; } - msg.Write(sub.EqualityCheckVal); + msg.WriteInt32(sub.EqualityCheckVal); if (sub.EqualityCheckVal == 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for - msg.Write(sub.MD5Hash.StringRepresentation); + msg.WriteString(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: if (!(data is GameModePreset gameMode)) { return; } - msg.Write(gameMode.Identifier); + msg.WriteIdentifier(gameMode.Identifier); break; case VoteType.EndRound: if (!(data is bool)) { return; } - msg.Write((bool)data); + msg.WriteBoolean((bool)data); break; case VoteType.Kick: if (!(data is Client votedClient)) { return; } - msg.Write(votedClient.ID); + msg.WriteByte(votedClient.SessionId); break; case VoteType.StartRound: if (!(data is bool)) { return; } - msg.Write((bool)data); + msg.WriteBoolean((bool)data); break; case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: @@ -148,22 +167,22 @@ namespace Barotrauma if (data is (SubmarineInfo voteSub, bool transferItems)) { //initiate sub vote - msg.Write(true); - msg.Write(voteSub.Name); - msg.Write(transferItems); + msg.WriteBoolean(true); + msg.WriteString(voteSub.Name); + msg.WriteBoolean(transferItems); } else { // vote if (!(data is int)) { return; } - msg.Write(false); - msg.Write((int)data); + msg.WriteBoolean(false); + msg.WriteInt32((int)data); } break; case VoteType.TransferMoney: if (!(data is int)) { return; } - msg.Write(false); //not initiating a vote - msg.Write((int)data); + msg.WriteBoolean(false); //not initiating a vote + msg.WriteInt32((int)data); break; } @@ -233,21 +252,22 @@ namespace Barotrauma DebugConsole.ThrowError("Failed to cast vote type \"" + voteTypeByte + "\"", e); } - byte yesClientCount = inc.ReadByte(); - for (int i = 0; i < yesClientCount; i++) + int readVote(int value) { - byte clientID = inc.ReadByte(); - var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); - matchingClient?.SetVote(voteType, 2); - } + byte clientCount = inc.ReadByte(); + for (int i = 0; i < clientCount; i++) + { + byte clientId = inc.ReadByte(); + var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == clientId); + matchingClient?.SetVote(voteType, value); + } - byte noClientCount = inc.ReadByte(); - for (int i = 0; i < noClientCount; i++) - { - byte clientID = inc.ReadByte(); - var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); - matchingClient?.SetVote(voteType, 1); + return clientCount; } + + int yesClientCount = readVote(value: 2); + int noClientCount = readVote(value: 1); + byte maxClientCount = inc.ReadByte(); SetVoteCountYes(voteType, yesClientCount); @@ -258,10 +278,10 @@ namespace Barotrauma { case VoteState.Started: byte starterID = inc.ReadByte(); - Client starterClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == starterID); + Client starterClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == starterID); float timeOut = inc.ReadByte(); - Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); + Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == GameMain.Client.SessionId); if (myClient == null || !myClient.InGame) { return; } switch (voteType) @@ -284,8 +304,8 @@ namespace Barotrauma byte toClientId = inc.ReadByte(); int transferAmount = inc.ReadInt32(); - Client fromClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == fromClientId); - Client toClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == toClientId); + Client fromClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == fromClientId); + Client toClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == toClientId); GameMain.Client.ShowMoneyTransferVoteInterface(starterClient, fromClient, transferAmount, toClient, timeOut); break; } @@ -343,8 +363,8 @@ namespace Barotrauma byte readyClientCount = inc.ReadByte(); for (int i = 0; i < readyClientCount; i++) { - byte clientID = inc.ReadByte(); - var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); + byte clientId = inc.ReadByte(); + var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == clientId); matchingClient?.SetVote(VoteType.StartRound, true); } UpdateVoteTexts(GameMain.NetworkMember.ConnectedClients, VoteType.StartRound); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs deleted file mode 100644 index 554d32169..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs +++ /dev/null @@ -1,245 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.Networking -{ - partial class WhiteListedPlayer - { - public WhiteListedPlayer(string name, UInt16 identifier, string ip) - { - Name = name; - IP = ip; - - UniqueIdentifier = identifier; - } - } - - partial class WhiteList - { - private GUIComponent whitelistFrame; - - private GUITextBox nameBox; - private GUITextBox ipBox; - private GUIButton addNewButton; - - public class LocalAdded - { - public string Name; - public string IP; - }; - - public bool localEnabled; - public List localRemoved = new List(); - public List localAdded = new List(); - - public GUIComponent CreateWhiteListFrame(GUIComponent parent) - { - if (whitelistFrame != null) - { - whitelistFrame.Parent.ClearChildren(); - whitelistFrame = null; - } - - whitelistFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), parent.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var enabledTick = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.1f), whitelistFrame.RectTransform), TextManager.Get("WhiteListEnabled")) - { - Selected = localEnabled, - UpdateOrder = 1, - OnSelected = (GUITickBox box) => - { - nameBox.Enabled = box.Selected; - ipBox.Enabled = box.Selected; - addNewButton.Enabled = box.Selected && !string.IsNullOrEmpty(ipBox.Text) && !string.IsNullOrEmpty(nameBox.Text); - localEnabled = box.Selected; - return true; - } - }; - - var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), whitelistFrame.RectTransform)); - foreach (WhiteListedPlayer wlp in whitelistedPlayers) - { - if (localRemoved.Contains(wlp.UniqueIdentifier)) continue; - string blockText = wlp.Name; - if (!string.IsNullOrWhiteSpace(wlp.IP)) blockText += " (" + wlp.IP + ")"; - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), - blockText) - { - UserData = wlp - }; - - var removeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get("WhiteListRemove"), style: "GUIButtonSmall") - { - UserData = wlp, - OnClicked = RemoveFromWhiteList - }; - } - - foreach (LocalAdded lad in localAdded) - { - string blockText = lad.Name; - if (!string.IsNullOrWhiteSpace(lad.IP)) blockText += " (" + lad.IP + ")"; - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), - blockText) - { - UserData = lad - }; - - var removeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get("WhiteListRemove"), style: "GUIButtonSmall") - { - UserData = lad, - OnClicked = RemoveFromWhiteList - }; - } - - foreach (GUIComponent c in listBox.Content.Children) - { - c.RectTransform.MinSize = new Point(0, Math.Max((int)(20 * GUI.Scale), c.RectTransform.Children.Max(c2 => c2.MinSize.Y))); - } - - var nameArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), whitelistFrame.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), nameArea.RectTransform), TextManager.Get("WhiteListName")); - nameBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1.0f), nameArea.RectTransform), ""); - nameBox.OnTextChanged += (textBox, text) => - { - addNewButton.Enabled = !string.IsNullOrEmpty(ipBox.Text) && !string.IsNullOrEmpty(nameBox.Text); - return true; - }; - nameArea.RectTransform.MinSize = new Point(0, nameBox.RectTransform.MinSize.Y); - - var ipArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), whitelistFrame.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), ipArea.RectTransform), TextManager.Get("WhiteListIP")); - ipBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1.0f), ipArea.RectTransform), ""); - ipBox.OnTextChanged += (textBox, text) => - { - addNewButton.Enabled = !string.IsNullOrEmpty(ipBox.Text) && !string.IsNullOrEmpty(nameBox.Text); - return true; - }; - ipBox.RectTransform.MinSize = new Point(0, ipBox.RectTransform.MinSize.Y); - - addNewButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), whitelistFrame.RectTransform), TextManager.Get("WhiteListAdd"), style: "GUIButtonSmall") - { - OnClicked = AddToWhiteList - }; - GUITextBlock.AutoScaleAndNormalize(addNewButton.TextBlock); - - nameBox.Enabled = localEnabled; - ipBox.Enabled = localEnabled; - addNewButton.Enabled = false; - - return parent; - } - - private bool RemoveFromWhiteList(GUIButton button, object obj) - { - if (obj is WhiteListedPlayer) - { - if (!(obj is WhiteListedPlayer wlp)) return false; - if (!localRemoved.Contains(wlp.UniqueIdentifier)) localRemoved.Add(wlp.UniqueIdentifier); - } - else if (obj is LocalAdded) - { - if (!(obj is LocalAdded lad)) return false; - if (localAdded.Contains(lad)) localAdded.Remove(lad); - } - - if (whitelistFrame != null) - { - CreateWhiteListFrame(whitelistFrame.Parent); - } - - return true; - } - - private bool AddToWhiteList(GUIButton button, object obj) - { - if (string.IsNullOrWhiteSpace(nameBox.Text)) return false; - if (whitelistedPlayers.Any(x => x.Name.ToLower() == nameBox.Text.ToLower() && x.IP == ipBox.Text)) return false; - - if (!localAdded.Any(p => p.IP == ipBox.Text)) localAdded.Add(new LocalAdded() { Name = nameBox.Text, IP = ipBox.Text }); - - if (whitelistFrame != null) - { - CreateWhiteListFrame(whitelistFrame.Parent); - } - return true; - } - - public void ClientAdminRead(IReadMessage incMsg) - { - bool hasPermission = incMsg.ReadBoolean(); - if (!hasPermission) - { - incMsg.ReadPadBits(); - return; - } - - bool isOwner = incMsg.ReadBoolean(); - localEnabled = incMsg.ReadBoolean(); - Enabled = localEnabled; - incMsg.ReadPadBits(); - - whitelistedPlayers.Clear(); - UInt32 bannedPlayerCount = incMsg.ReadVariableUInt32(); - for (int i = 0; i < (int)bannedPlayerCount; i++) - { - string name = incMsg.ReadString(); - UInt16 uniqueIdentifier = incMsg.ReadUInt16(); - - string ip = ""; - if (isOwner) - { - ip = incMsg.ReadString(); - } - else - { - ip = "IP concealed by host"; - } - whitelistedPlayers.Add(new WhiteListedPlayer(name, uniqueIdentifier, ip)); - } - - if (whitelistFrame != null) - { - CreateWhiteListFrame(whitelistFrame.Parent); - } - } - - public void ClientAdminWrite(IWriteMessage outMsg) - { - outMsg.Write(localEnabled); - outMsg.WritePadBits(); - - outMsg.Write((UInt16)localRemoved.Count); - foreach (UInt16 uniqueId in localRemoved) - { - outMsg.Write(uniqueId); - } - - outMsg.Write((UInt16)localAdded.Count); - foreach (LocalAdded lad in localAdded) - { - outMsg.Write(lad.Name); - outMsg.Write(lad.IP); //TODO: ENCRYPT - } - - localRemoved.Clear(); - localAdded.Clear(); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 982106567..dfdd7342a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Particles { @@ -217,6 +216,11 @@ namespace Barotrauma.Particles } } + public void ClearParticles() + { + particleCount = 0; + } + public void RemoveByPrefab(ParticlePrefab prefab) { if (particles == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index c84e3b896..6efac8aeb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -132,11 +132,11 @@ namespace Barotrauma } else if (a.MouseButton != MouseButton.None) { - return a.MouseButton == b.MouseButton; + return !(b is null) && a.MouseButton == b.MouseButton; } else { - return a.Key.Equals(b.Key); + return !(b is null) && a.Key.Equals(b.Key); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 0b33e0a5a..46dfef1ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -61,7 +61,8 @@ namespace Barotrauma UserData = saveInfo.FilePath }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath)) + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), + textColor: GUIStyle.TextColorBright) { CanBeFocused = false }; @@ -85,7 +86,6 @@ namespace Barotrauma UserData = saveInfo.FilePath }; - string saveTimeStr = string.Empty; if (saveInfo.SaveTime > 0) { @@ -187,9 +187,9 @@ namespace Barotrauma SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); ImmutableArray> fundOptions = ImmutableArray.Create( - new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high"), + new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), - new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low") + new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high") ); SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 0c297bbf1..70a8bbd8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -11,7 +11,9 @@ namespace Barotrauma class MultiPlayerCampaignSetupUI : CampaignSetupUI { private GUIButton deleteMpSaveButton; - + + private int prevInitialMoney; + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, List saveFiles = null) : base(newGameContainer, loadGameContainer) { @@ -133,6 +135,7 @@ namespace Barotrauma StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + prevInitialMoney = 8000; InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => @@ -142,11 +145,17 @@ namespace Barotrauma { initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); } + if (prevInitialMoney != initialMoney) + { + GameMain.NetLobbyScreen.RefreshEnabledElements(); + prevInitialMoney = initialMoney; + } if (GameMain.NetLobbyScreen.SelectedSub != null) { initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; } - initialMoney = Math.Max(initialMoney, MultiPlayerCampaign.MinimumInitialMoney); + initialMoney = Math.Max(initialMoney, 0); + return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 7f98cdc43..5688ea044 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -476,8 +476,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - SubmarineInfo sub = child.UserData as SubmarineInfo; - if (sub == null) { return; } + if (!(child.UserData is SubmarineInfo sub)) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -523,9 +522,11 @@ namespace Barotrauma subsToShow.Sort((s1, s2) => { - int p1 = s1.Price > CurrentSettings.InitialMoney ? 10 : 0; - int p2 = s2.Price > CurrentSettings.InitialMoney ? 10 : 0; - return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); + int p1 = s1.Price; + if (!s1.IsCampaignCompatible) { p1 += 100000; } + int p2 = s2.Price; + if (!s2.IsCampaignCompatible) { p2 += 100000; } + return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); subList.ClearChildren(); @@ -533,7 +534,7 @@ namespace Barotrauma foreach (SubmarineInfo sub in subsToShow) { var textBlock = new GUITextBlock( - new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, + new RectTransform(new Vector2(1, 0.15f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), style: "ListBoxElement") { ToolTip = sub.Description, @@ -546,12 +547,19 @@ namespace Barotrauma textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.ToolTip.SanitizedString; } - var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) + var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), isHorizontal: false); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.TopRight, font: GUIStyle.SmallFont) + { + TextColor = textBlock.TextColor * 0.8f, + ToolTip = textBlock.ToolTip + }; #if !DEBUG if (!GameMain.DebugDraw) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index ba4d9eaa3..a405d730e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -27,8 +27,6 @@ namespace Barotrauma private bool hasMaxMissions; - private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; - private SubmarineSelection submarineSelection; private Location selectedLocation; @@ -101,170 +99,6 @@ namespace Barotrauma tabs[(int)CampaignMode.InteractionType.Store] = storeTab; Store = new Store(this, storeTab); - // repair tab ------------------------------------------------------------------------- - - tabs[(int)CampaignMode.InteractionType.Repair] = CreateDefaultTabContainer(container, new Vector2(0.7f)); - var repairFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Repair).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), repairFrame.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) - { - UserData = "outerglow", - CanBeFocused = false - }; - - var repairContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), repairFrame.RectTransform, Anchor.Center)) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), repairContent.RectTransform), "", font: GUIStyle.LargeFont) - { - TextGetter = GetMoney - }; - - // repair hulls ----------------------------------------------- - - var repairHullsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUIImage(new RectTransform(new Vector2(0.3f, 1.0f), repairHullsHolder.RectTransform, Anchor.CenterLeft), "RepairHullButton") - { - IgnoreLayoutGroups = true, - CanBeFocused = false - }; - var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) - { - ForceUpperCase = ForceUpperCase.Yes - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); - repairHullsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairHullsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) - { - OnClicked = (btn, userdata) => - { - if (Campaign.PurchasedHullRepairs) - { - Campaign.Wallet.Refund(CampaignMode.HullRepairCost); - Campaign.PurchasedHullRepairs = false; - } - else - { - if (Campaign.TryPurchase(null, CampaignMode.HullRepairCost)) - { - GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - Campaign.PurchasedHullRepairs = true; - } - } - GameMain.Client?.SendCampaignState(); - btn.GetChild().Selected = Campaign.PurchasedHullRepairs; - - return true; - } - }; - new GUITickBox(new RectTransform(new Vector2(0.65f), repairHullsButton.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(10, 0) }, "") - { - CanBeFocused = false - }; - - // repair items ------------------------------------------- - - var repairItemsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUIImage(new RectTransform(new Vector2(0.3f, 1.0f), repairItemsHolder.RectTransform, Anchor.CenterLeft), "RepairItemsButton") - { - IgnoreLayoutGroups = true, - CanBeFocused = false - }; - var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) - { - ForceUpperCase = ForceUpperCase.Yes - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); - repairItemsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairItemsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) - { - OnClicked = (btn, userdata) => - { - if (Campaign.PurchasedItemRepairs) - { - Campaign.Wallet.Refund(CampaignMode.ItemRepairCost); - Campaign.PurchasedItemRepairs = false; - } - else - { - if (Campaign.TryPurchase(null, CampaignMode.ItemRepairCost)) - { - GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - Campaign.PurchasedItemRepairs = true; - } - } - GameMain.Client?.SendCampaignState(); - btn.GetChild().Selected = Campaign.PurchasedItemRepairs; - - return true; - } - }; - new GUITickBox(new RectTransform(new Vector2(0.65f), repairItemsButton.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(10, 0) }, "") - { - CanBeFocused = false - }; - - // replace lost shuttles ------------------------------------------- - - var replaceShuttlesHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUIImage(new RectTransform(new Vector2(0.3f, 1.0f), replaceShuttlesHolder.RectTransform, Anchor.CenterLeft), "ReplaceShuttlesButton") - { - IgnoreLayoutGroups = true, - CanBeFocused = false - }; - var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) - { - ForceUpperCase = ForceUpperCase.Yes - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); - replaceShuttlesButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), replaceShuttlesHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("ReplaceShuttles")) - { - OnClicked = (btn, userdata) => - { - if (GameMain.GameSession?.SubmarineInfo != null && - GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) - { - new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied")); - return true; - } - - if (Campaign.PurchasedLostShuttles) - { - Campaign.Wallet.Refund(CampaignMode.ShuttleReplaceCost); - Campaign.PurchasedLostShuttles = false; - } - else - { - if (Campaign.TryPurchase(null, CampaignMode.ShuttleReplaceCost)) - { - GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); - Campaign.PurchasedLostShuttles = true; - } - } - GameMain.Client?.SendCampaignState(); - btn.GetChild().Selected = Campaign.PurchasedLostShuttles; - - return true; - } - }; - new GUITickBox(new RectTransform(new Vector2(0.65f), replaceShuttlesButton.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(10, 0) }, "") - { - CanBeFocused = false - }; - GUITextBlock.AutoScaleAndNormalize(repairHullsLabel, repairItemsLabel, replaceShuttlesLabel); - GUITextBlock.AutoScaleAndNormalize(repairHullsButton.GetChild().TextBlock, repairItemsButton.GetChild().TextBlock, replaceShuttlesButton.GetChild().TextBlock); - // upgrade tab ------------------------------------------------------------------------- tabs[(int)CampaignMode.InteractionType.Upgrade] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); @@ -512,7 +346,7 @@ namespace Barotrauma }; var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); - // missionName.RectTransform.MinSize = new Point(0, (int)(missionName.Rect.Height * 1.5f)); + missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(15)); if (mission != null) { var tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) @@ -701,26 +535,6 @@ namespace Barotrauma switch (selectedTab) { - case CampaignMode.InteractionType.Repair: - repairHullsButton.Enabled = - (Campaign.PurchasedHullRepairs || Campaign.Wallet.CanAfford(CampaignMode.HullRepairCost)); - repairHullsButton.GetChild().Selected = Campaign.PurchasedHullRepairs; - repairItemsButton.Enabled = - (Campaign.PurchasedItemRepairs || Campaign.Wallet.CanAfford(CampaignMode.ItemRepairCost)); - repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; - - if (GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind) - { - replaceShuttlesButton.Enabled = false; - replaceShuttlesButton.GetChild().Selected = false; - } - else - { - replaceShuttlesButton.Enabled = - (Campaign.PurchasedLostShuttles || Campaign.Wallet.CanAfford(CampaignMode.ShuttleReplaceCost)); - replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; - } - break; case CampaignMode.InteractionType.Store: Store.SelectStore(storeIdentifier); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 11105e73a..668226715 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2606,8 +2606,8 @@ namespace Barotrauma.CharacterEditor animationControls = new GUIFrame(new RectTransform(Vector2.One, centerArea.RectTransform), style: null) { CanBeFocused = false }; var layoutGroupAnimation = new GUILayoutGroup(new RectTransform(Vector2.One, animationControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false }; var animationSelectionElement = new GUIFrame(new RectTransform(new Point(elementSize.X * 2 - (int)(5 * GUI.xScale), elementSize.Y), layoutGroupAnimation.RectTransform), style: null); - var animationSelectionText = new GUITextBlock(new RectTransform(new Point(elementSize.X, elementSize.Y), animationSelectionElement.RectTransform), GetCharacterEditorTranslation("SelectedAnimation") + ": ", Color.WhiteSmoke, textAlignment: Alignment.Center); - animSelection = new GUIDropDown(new RectTransform(new Point((int)(100 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.TopRight), elementCount: 5); + var animationSelectionText = new GUITextBlock(new RectTransform(new Point(elementSize.X, elementSize.Y), animationSelectionElement.RectTransform), GetCharacterEditorTranslation("SelectedAnimation"), Color.WhiteSmoke, textAlignment: Alignment.CenterRight); + animSelection = new GUIDropDown(new RectTransform(new Point((int)(150 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.Center, Pivot.CenterLeft), elementCount: 5); if (character.AnimController.CanWalk) { animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 22b86c510..620f10795 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -179,7 +179,7 @@ namespace Barotrauma // Ok button msgBox.Buttons[1].OnClicked = delegate { - foreach (var illegalChar in Path.GetInvalidFileNameChars()) + foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (!nameInput.Text.Contains(illegalChar)) { continue; } @@ -274,7 +274,7 @@ namespace Barotrauma // Ok button msgBox.Buttons[1].OnClicked = delegate { - foreach (var illegalChar in Path.GetInvalidFileNameChars()) + foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (!nameInput.Text.Contains(illegalChar)) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index ce6d5ffda..54d5b2d8e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,11 +1,10 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; -using FarseerPhysics; using System.Diagnostics; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -79,21 +78,27 @@ namespace Barotrauma public override void AddToGUIUpdateList() { - if (Character.Controlled != null && Character.Controlled.SelectedConstruction != null && Character.Controlled.CanInteractWith(Character.Controlled.SelectedConstruction)) + if (Character.Controlled != null) { - Character.Controlled.SelectedConstruction.AddToGUIUpdateList(); - } - if (Character.Controlled?.Inventory != null) - { - foreach (Item item in Character.Controlled.Inventory.AllItems) + if (Character.Controlled.SelectedItem is { } selectedItem && Character.Controlled.CanInteractWith(selectedItem)) { - if (Character.Controlled.HasEquippedItem(item)) + selectedItem.AddToGUIUpdateList(); + } + if (Character.Controlled.SelectedSecondaryItem is { } selectedSecondaryItem && Character.Controlled.CanInteractWith(selectedSecondaryItem)) + { + selectedSecondaryItem.AddToGUIUpdateList(); + } + if (Character.Controlled.Inventory != null) + { + foreach (Item item in Character.Controlled.Inventory.AllItems) { - item.AddToGUIUpdateList(); + if (Character.Controlled.HasEquippedItem(item)) + { + item.AddToGUIUpdateList(); + } } } } - GameMain.GameSession?.AddToGUIUpdateList(); Character.AddAllToGUIUpdateList(); base.AddToGUIUpdateList(); @@ -261,11 +266,7 @@ namespace Barotrauma //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); - foreach (Character c in Character.CharacterList) - { - if (!c.IsVisible || c.AnimController.Limbs.Any(l => l.DeformSprite != null)) { continue; } - c.Draw(spriteBatch, Cam); - } + DrawCharacters(deformed: false, firstPass: true); spriteBatch.End(); sw.Stop(); @@ -273,11 +274,12 @@ namespace Barotrauma sw.Restart(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - DrawDeformed(firstPass: true); - DrawDeformed(firstPass: false); + DrawCharacters(deformed: true, firstPass: true); + DrawCharacters(deformed: true, firstPass: false); + DrawCharacters(deformed: false, firstPass: false); spriteBatch.End(); - void DrawDeformed(bool firstPass) + void DrawCharacters(bool deformed, bool firstPass) { //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) for (int i = Character.CharacterList.Count - 1; i >= 0; i--) @@ -285,7 +287,14 @@ namespace Barotrauma Character c = Character.CharacterList[i]; if (!c.IsVisible) { continue; } if (c.Params.DrawLast == firstPass) { continue; } - if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + if (deformed) + { + if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + } + else + { + if (c.AnimController.Limbs.Any(l => l.DeformSprite != null)) { continue; } + } c.Draw(spriteBatch, Cam); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 3d026e56a..4537be4ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -810,7 +810,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUIStyle.LargeFont); } - // TODO: Improve this temporary level editor debug solution (or remove it) + // TODO: Improve this temporary level editor debug solution foreach (var pathPoint in Level.Loaded.PathPoints) { Vector2 pathPointPos = new Vector2(pathPoint.Position.X, -pathPoint.Position.Y); @@ -833,6 +833,17 @@ namespace Barotrauma GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUIStyle.LargeFont); } + foreach (var location in Level.Loaded.AbyssResources) + { + if (location.Resources == null) { continue; } + foreach (var resource in location.Resources) + { + Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); + spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUIStyle.LargeFont); + } + } + /*for (int i = 0; i < Level.Loaded.distanceField.Count; i++) { GUI.DrawRectangle(spriteBatch, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 8569b995c..c2d8b96d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -67,6 +67,10 @@ namespace Barotrauma public static readonly Queue WorkshopItemsToUpdate = new Queue(); + private GUIImage tutorialBanner; + private GUITextBlock tutorialHeader, tutorialDescription; + private GUIListBox tutorialList; + #region Creation public MainMenuScreen(GameMain game) { @@ -390,7 +394,7 @@ namespace Barotrauma SelectTab(tb, userdata); GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), - IPAddress.Loopback.ToString(), 0, "localhost", 0, false); + new LidgrenEndpoint(IPAddress.Loopback, NetConfig.DefaultPort), "localhost", Option.None()); return true; } @@ -449,34 +453,7 @@ namespace Barotrauma //---------------------------------------------------------------------- menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - - //PLACEHOLDER - var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) - { - PlaySoundOnSelect = true, - }; - var tutorialTypes = new List() - { - typeof(MechanicTutorial), - typeof(EngineerTutorial), - typeof(DoctorTutorial), - typeof(OfficerTutorial), - typeof(CaptainTutorial), - }; - foreach (Type tutorialType in tutorialTypes) - { - Tutorial tutorial = (Tutorial)Activator.CreateInstance(tutorialType); - var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont) - { - UserData = tutorial - }; - } - tutorialList.OnSelected += (component, obj) => - { - (obj as Tutorial).Start(); - return true; - }; + CreateTutorialTab(); this.game = game; @@ -492,7 +469,72 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); } -#endregion + + private void CreateTutorialTab() + { + var tutorialInnerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.Tutorials].RectTransform, Anchor.Center), style: "InnerFrame"); + var tutorialContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tutorialInnerFrame.RectTransform, Anchor.Center), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; + + tutorialList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), tutorialContent.RectTransform)) + { + PlaySoundOnSelect = true, + OnSelected = (component, obj) => + { + SelectTutorial(obj as Tutorial); + return true; + } + }; + var tutorialPreview = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), tutorialContent.RectTransform)) { RelativeSpacing = 0.05f, Stretch = true }; + var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), tutorialPreview.RectTransform), style: "InnerFrame"); + tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: true); + + var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); + + tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.75f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + + var startButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.0f), infoContent.RectTransform, Anchor.BottomRight), text: TextManager.Get("startgamebutton")) + { + IgnoreLayoutGroups = true, + OnClicked = (component, obj) => + { + (tutorialList.SelectedData as Tutorial)?.Start(); + return true; + } + }; + + Tutorial firstTutorial = null; + foreach (var tutorialPrefab in TutorialPrefab.Prefabs.OrderBy(p => p.Order)) + { + var tutorial = new Tutorial(tutorialPrefab); + firstTutorial ??= tutorial; + var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), tutorialList.Content.RectTransform), tutorial.DisplayName) + { + Padding = new Vector4(30.0f * GUI.Scale, 0,0,0), + UserData = tutorial + }; + tutorialText.RectTransform.MinSize = new Point(0, (int)(tutorialText.TextSize.Y * 2)); + } + GUITextBlock.AutoScaleAndNormalize(tutorialList.Content.Children.Select(c => c as GUITextBlock)); + tutorialList.Select(firstTutorial); + } + + private void SelectTutorial(Tutorial tutorial) + { + tutorialHeader.Text = tutorial.DisplayName; + tutorial.TutorialPrefab.Banner?.EnsureLazyLoaded(); + tutorialBanner.Sprite = tutorial.TutorialPrefab.Banner; + tutorialBanner.Color = tutorial.TutorialPrefab.Banner == null ? Color.Black : Color.White; + } + + public static void UpdateInstanceTutorialButtons() + { + if (GameMain.MainMenuScreen is not MainMenuScreen menuScreen) { return; } + menuScreen.tutorialList.ClearChildren(); + menuScreen.CreateTutorialTab(); + } + + #endregion #region Selection public override void Select() @@ -513,7 +555,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.Disconnect(); + GameMain.Client.Quit(); GameMain.Client = null; } @@ -719,8 +761,13 @@ namespace Barotrauma gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty, levelGenerationParams); GameMain.GameScreen.Select(); // TODO: modding support - string[] jobIdentifiers = new string[] { "captain", "engineer", "mechanic", "securityofficer", "medicaldoctor" }; - foreach (string job in jobIdentifiers) + Identifier[] jobIdentifiers = new Identifier[] { + "captain".ToIdentifier(), + "engineer".ToIdentifier(), + "mechanic".ToIdentifier(), + "securityofficer".ToIdentifier(), + "medicaldoctor".ToIdentifier() }; + foreach (Identifier job in jobIdentifiers) { var jobPrefab = JobPrefab.Get(job); var variant = Rand.Range(0, jobPrefab.Variants); @@ -756,33 +803,12 @@ namespace Barotrauma private void UpdateTutorialList() { - var tutorialList = menuTabs[Tab.Tutorials].GetChild(); - - int completedTutorials = 0; - foreach (GUITextBlock tutorialText in tutorialList.Content.Children) { - if (CompletedTutorials.Instance.Contains(((Tutorial)tutorialText.UserData).Identifier)) + var tutorial = (Tutorial)tutorialText.UserData; + if (CompletedTutorials.Instance.Contains(tutorial.Identifier) && tutorialText.GetChild() == null) { - completedTutorials++; - } - } - - for (int i = 0; i < tutorialList.Content.Children.Count(); i++) - { - if (i < completedTutorials + 1) - { - (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = GUIStyle.Green; -#if !DEBUG - (tutorialList.Content.GetChild(i) as GUITextBlock).CanBeFocused = true; -#endif - } - else - { - (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = Color.Gray; -#if !DEBUG - (tutorialList.Content.GetChild(i) as GUITextBlock).CanBeFocused = false; -#endif + new GUIImage(new RectTransform(new Point((int)(tutorialText.Padding.X * 0.8f)), tutorialText.RectTransform, Anchor.CenterLeft), style: "ObjectiveIndicatorCompleted"); } } } @@ -853,9 +879,9 @@ namespace Barotrauma arguments += " -nopassword"; } - if (Steam.SteamManager.GetSteamID() != 0) + if (SteamManager.GetSteamId().TryUnwrap(out var steamId1)) { - arguments += " -steamid " + Steam.SteamManager.GetSteamID(); + arguments += " -steamid " + steamId1.Value; } int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments += " -ownerkey " + ownerKey; @@ -884,8 +910,12 @@ namespace Barotrauma Thread.Sleep(1000); //wait until the server is ready before connecting GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty( - SteamManager.GetUsername().FallbackNullOrEmpty(name)), - System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); + SteamManager.GetUsername().FallbackNullOrEmpty(name)), + SteamManager.GetSteamId().TryUnwrap(out var steamId) + ? new SteamP2PEndpoint(steamId) + : (Endpoint)new LidgrenEndpoint(IPAddress.Loopback, NetConfig.DefaultPort), + name, + Option.Some(ownerKey)); } catch (Exception e) { @@ -1165,7 +1195,7 @@ namespace Barotrauma var playstyleContainer = new GUIFrame(new RectTransform(new Vector2(1.35f, 0.1f), parent.RectTransform), style: null, color: Color.Black); playstyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 0.1f), playstyleContainer.RectTransform), - ServerListScreen.PlayStyleBanners[0], scaleToFit: true) + GUIStyle.GetComponentStyle($"PlayStyleBanner.{PlayStyle.Serious}").GetSprite(GUIComponent.ComponentState.None), scaleToFit: true) { UserData = PlayStyle.Serious }; @@ -1384,12 +1414,15 @@ namespace Barotrauma private void SetServerPlayStyle(PlayStyle playStyle) { - playstyleBanner.Sprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; + playstyleBanner.Sprite = GUIStyle + .GetComponentStyle($"PlayStyleBanner.{playStyle}") + .GetSprite(GUIComponent.ComponentState.None); playstyleBanner.UserData = playStyle; var nameText = playstyleBanner.GetChild(); nameText.Text = TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag." + playStyle)); - nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.Color = playstyleBanner.Sprite + .SourceElement.GetAttributeColor("BannerColor") ?? Color.White; nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 7ca1afd98..ff723980e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -9,7 +9,7 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Color = Microsoft.Xna.Framework.Color; -using ServerContentPackage = Barotrauma.Networking.ClientPeer.ServerContentPackage; +using ServerContentPackage = Barotrauma.Networking.ServerContentPackage; namespace Barotrauma { @@ -21,7 +21,7 @@ namespace Barotrauma private readonly List downloadedPackages = new List(); public IEnumerable DownloadedPackages => downloadedPackages; - + private bool confirmDownload; public void Reset() @@ -68,15 +68,31 @@ namespace Barotrauma { OnClicked = (guiButton, o) => { - GameMain.Client?.Disconnect(); + GameMain.Client?.Quit(); GameMain.MainMenuScreen.Select(); return false; } }; - + + if (!GameMain.Client.IsServerOwner) + { + if (GameMain.Client.ClientPeer.ServerContentPackages.Length == 0) + { + string errorMsg = $"Error in ModDownloadScreen: the list of mods the server has enabled was empty. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; + GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new InvalidOperationException(errorMsg); + } + if (GameMain.Client.ClientPeer.ServerContentPackages.None(p => p.CorePackage != null)) + { + string errorMsg = $"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; + GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoCorePackage", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new InvalidOperationException(errorMsg); + } + } + var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages .Where(sp => sp.ContentPackage is null).ToArray(); - if (!missingPackages.Any()) + if (!missingPackages.Any(p => p.IsMandatory)) { if (!GameMain.Client.IsServerOwner) { @@ -84,11 +100,14 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.SetCore( GameMain.Client.ClientPeer.ServerContentPackages .Select(p => p.CorePackage) - .First(p => p != null)); - ContentPackageManager.EnabledPackages.SetRegular( + .OfType().First()); + List regularPackages = GameMain.Client.ClientPeer.ServerContentPackages .Select(p => p.RegularPackage) - .Where(p => p != null).ToArray()); + .OfType().ToList(); + //keep enabled client-side-only mods enabled + regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p))); + ContentPackageManager.EnabledPackages.SetRegular(regularPackages); } GameMain.NetLobbyScreen.Select(); GameMain.LuaCs.Initialize(); @@ -154,16 +173,16 @@ namespace Barotrauma buttonContainerSpacing(0.2f); button(TextManager.Get("No"), () => { - GameMain.Client?.Disconnect(); + GameMain.Client?.Quit(); GameMain.MainMenuScreen.Select(); }); buttonContainerSpacing(0.1f); - var missingIds = missingPackages.Where( - mp => mp.WorkshopId != 0 - && ContentPackageManager.WorkshopPackages.All(wp - => wp.SteamWorkshopId != mp.WorkshopId)) - .Select(mp => mp.WorkshopId) + var missingIds = missingPackages + .Where(p => p.IsMandatory) + .Select(mp => ContentPackageId.Parse(mp.UgcId)) + .NotNone() + .Where(id => ContentPackageManager.WorkshopPackages.All(wp => !wp.UgcId.Equals(id))) .ToArray(); if (missingIds.Any() && SteamManager.IsInitialized) { @@ -173,18 +192,18 @@ namespace Barotrauma { if (GameMain.Client != null) { - BulkDownloader.SubscribeToServerMods(missingIds, - rejoinEndpoint: GameMain.Client.ClientPeer.ServerConnection.EndPointString, - rejoinLobby: SteamManager.CurrentLobbyID, - rejoinServerName: GameMain.NetLobbyScreen.ServerName.Text); - GameMain.Client.Disconnect(); + BulkDownloader.SubscribeToServerMods(missingIds.OfType().Select(id => id.Value), + new ConnectCommand( + serverName: GameMain.Client.ServerName, + endpoint: GameMain.Client.ClientPeer.ServerEndpoint)); + GameMain.Client.Quit(); } GameMain.MainMenuScreen.Select(); }, width: 0.7f); buttonContainerSpacing(0.15f); } - foreach (var p in missingPackages) + foreach (var p in missingPackages.Where(p => p.IsMandatory)) { pendingDownloads.Enqueue(p); @@ -276,23 +295,50 @@ namespace Barotrauma ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) ?.CorePackage ?? throw new Exception($"Failed to find core package to enable"); - RegularPackage[] regularPackages - = serverPackages.Where(p => p.CorePackage is null) - .Select(p => - p.RegularPackage - ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) - ?? throw new Exception($"Could not find regular package \"{p.Name}\"")) - .Cast() - .ToArray(); + + List regularPackages = new List(); + foreach (var p in serverPackages) + { + if (p.CorePackage != null) { continue; } + RegularPackage? matchingPackage = + p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage; + if (matchingPackage is null) + { + if (!p.IsMandatory) + { + //we don't need to care about missing non-mandatory (= submarine) mods + continue; + } + else + { + throw new Exception($"Could not find regular package \"{p.Name}\""); + } + } + regularPackages.Add(matchingPackage); + } foreach (var regularPackage in regularPackages) { DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime); } + //keep enabled client-side-only mods enabled + regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p))); + ContentPackageManager.EnabledPackages.BackUp(); ContentPackageManager.EnabledPackages.SetCore(corePackage); ContentPackageManager.EnabledPackages.SetRegular(regularPackages); + //see if any of the packages we enabled contain subs that we were missing previously, and update their paths + foreach (var serverSub in GameMain.Client.ServerSubmarines) + { + if (File.Exists(serverSub.FilePath)) { continue; } + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSub.Name && s.MD5Hash == serverSub.MD5Hash); + if (matchingSub != null) + { + serverSub.FilePath = matchingSub.FilePath; + } + } + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, GameMain.Client.ServerSubmarines); GameMain.NetLobbyScreen.Select(); GameMain.LuaCs.Initialize(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 4bf99f6d8..9471d8128 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma @@ -665,7 +666,7 @@ namespace Barotrauma OnSelected = (tickbox) => { if (GameMain.Client == null) { return true; } - ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); + ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); if (tickbox.Selected) { GameMain.ServerListScreen.AddToFavoriteServers(info); @@ -866,7 +867,7 @@ namespace Barotrauma { OnSelected = (component, obj) => { - GameMain.Client?.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: true); + GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); return true; } }; @@ -1431,10 +1432,6 @@ namespace Barotrauma 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) { @@ -1453,7 +1450,6 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } string newName = Client.SanitizeName(tb.Text); - newName = newName.Replace(":", "").Replace(";", ""); if (newName == GameMain.Client.Name) return; if (string.IsNullOrWhiteSpace(newName)) { @@ -1529,14 +1525,13 @@ namespace Barotrauma while (i < MultiplayerPreferences.Instance.JobPreferences.Count) { var jobPreference = MultiplayerPreferences.Instance.JobPreferences[i]; - if (!JobPrefab.Prefabs.ContainsKey(jobPreference.JobIdentifier)) + if (!JobPrefab.Prefabs.TryGet(jobPreference.JobIdentifier, out JobPrefab prefab) || prefab.HiddenJob) { MultiplayerPreferences.Instance.JobPreferences.RemoveAt(i); continue; } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist - var prefab = JobPrefab.Prefabs[jobPreference.JobIdentifier]; var variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); jobPrefab = new JobVariant(prefab, variant); break; @@ -1782,6 +1777,10 @@ namespace Barotrauma // Hide spectate tickbox if spectating is not allowed spectateBox.Visible = allowSpectating; + if (infoContainer != null) + { + infoContainer.RectTransform.RelativeSize = new Vector2(infoContainer.RectTransform.RelativeSize.X, spectateBox.Visible ? 0.92f : 0.97f); + } } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1795,7 +1794,7 @@ namespace Barotrauma MissionType = missionType; } - public void UpdateSubList(GUIComponent subList, List submarines) + public void UpdateSubList(GUIComponent subList, IEnumerable submarines) { if (subList == null) { return; } @@ -1818,7 +1817,7 @@ namespace Barotrauma subList = dropDown.ListBox.Content; } - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform) { MinSize = new Point(0, 20) }, + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), subList.RectTransform) { MinSize = new Point(0, 25) }, style: "ListBoxElement") { ToolTip = sub.Description, @@ -1874,7 +1873,7 @@ namespace Barotrauma { if (sub.HasTag(SubmarineTag.Shuttle)) { - var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, @@ -1882,7 +1881,7 @@ namespace Barotrauma CanBeFocused = false }; //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) - if (subList == this.SubList.Content) + if (subList == SubList.Content) { subTextBlock.TextColor *= 0.8f; foreach (GUIComponent child in parent.Children) @@ -1893,8 +1892,16 @@ namespace Barotrauma } else { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) + var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, isHorizontal: false); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) + { + UserData = "pricetext", + TextColor = subTextBlock.TextColor * 0.8f, + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.TopRight, font: GUIStyle.SmallFont) { UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, @@ -1914,6 +1921,17 @@ namespace Barotrauma if (!GameMain.Client.ServerSettings.AllowSubVoting) { var selectedSub = component.UserData as SubmarineInfo; + if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) + { + if (selectedSub.Price > CampaignSetupUI.CurrentSettings.InitialMoney) + { + new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); + } + if (!selectedSub.IsCampaignCompatible) + { + new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubincompatible")); + } + } if (!selectedSub.RequiredContentPackagesInstalled) { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), @@ -1925,7 +1943,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => { - GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: false); + GameMain.Client.RequestSelectSub(obj as SubmarineInfo, isShuttle: false); return true; }; msgBox.Buttons[1].OnClicked = msgBox.Close; @@ -1933,7 +1951,7 @@ namespace Barotrauma } else if (GameMain.Client.HasPermission(ClientPermissions.SelectSub)) { - GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: false); + GameMain.Client.RequestSelectSub(selectedSub, isShuttle: false); return true; } return false; @@ -2160,15 +2178,8 @@ namespace Barotrauma if (child != null) { PlayerList.RemoveChild(child); } } - private Client ExtractClientFromClickableArea(GUITextBlock.ClickableArea area) - { - if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return null; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) - ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); - return client; - } + public static Client ExtractClientFromClickableArea(GUITextBlock.ClickableArea area) + => area.Data.ExtractClient(); public void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area) { @@ -2188,29 +2199,35 @@ namespace Barotrauma public static void CreateModerationContextMenu(Client client) { if (GUIContextMenu.CurrentContextMenu != null) { return; } - if (GameMain.IsSingleplayer || client == null || ((!GameMain.Client?.PreviouslyConnectedClients?.Contains(client)) ?? true)) { return; } - 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); + if (GameMain.IsSingleplayer || client == null) { return; } + if (!(GameMain.Client is { PreviouslyConnectedClients: var previouslyConnectedClients }) + || !previouslyConnectedClients.Contains(client)) { return; } + + bool hasAccountId = client.AccountId.IsSome(); + bool canKick = GameMain.Client.HasPermission(ClientPermissions.Kick); + bool canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking; + bool canManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); // Disable options if we are targeting ourselves - if (client.ID == GameMain.Client?.ID) + if (client.SessionId == GameMain.Client.SessionId) { - canKick = canBan = canPromo = false; + canKick = canBan = canManagePermissions = false; } - List options = new List + List options = new List(); + + if (client.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) { - new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate - { - Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); - }), - new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: delegate + options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasAccountId, onSelected: () => + { + SteamManager.OverlayProfile(steamId); + })); + } + + options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: () => { GameMain.NetLobbyScreen?.SelectPlayer(client); - }) - }; + })); // Creates sub context menu options for all the ranks List rankOptions = new List(); @@ -2236,18 +2253,18 @@ namespace Barotrauma }) { Tooltip = rank.Description }); } - options.Add(new ContextMenuOption("Rank", isEnabled: canPromo, options: rankOptions.ToArray())); + options.Add(new ContextMenuOption("Rank", isEnabled: canManagePermissions, options: rankOptions.ToArray())); Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; if (GameMain.Client.ConnectedClients.Contains(client)) { - options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.ID != GameMain.Client?.ID, onSelected: delegate + options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.SessionId != GameMain.Client.SessionId, onSelected: delegate { client.MutedLocally = !client.MutedLocally; })); - bool kickEnabled = client.ID != GameMain.Client?.ID && client.AllowKicking; + bool kickEnabled = client.SessionId != GameMain.Client.SessionId && client.AllowKicking; // if the user can kick create a kick option else create the votekick option ContextMenuOption kickOption; @@ -2281,7 +2298,7 @@ namespace Barotrauma public bool SelectPlayer(Client selectedClient) { - bool myClient = selectedClient.ID == GameMain.Client.ID; + bool myClient = selectedClient.SessionId == GameMain.Client.SessionId; bool hasManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) @@ -2510,14 +2527,6 @@ namespace Barotrauma }; banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; banButton.OnClicked += ClosePlayerFrame; - - var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), - TextManager.Get("BanRange")) - { - UserData = selectedClient - }; - rangebanButton.OnClicked = (bt, userdata) => { BanPlayerRange(selectedClient); return true; }; - rangebanButton.OnClicked += ClosePlayerFrame; } if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) @@ -2528,7 +2537,6 @@ namespace Barotrauma var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("VoteToKick")) { - Enabled = !selectedClient.HasKickVoteFromID(GameMain.Client.ID), OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; }, UserData = selectedClient }; @@ -2560,7 +2568,7 @@ namespace Barotrauma } } - if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) + if (selectedClient.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId && Steam.SteamManager.IsInitialized) { var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, TextManager.Get("ViewSteamProfile")) @@ -2570,7 +2578,7 @@ namespace Barotrauma viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; viewSteamProfileButton.OnClicked = (bt, userdata) => { - SteamManager.OverlayCustomURL("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + SteamManager.OverlayProfile(steamId); return true; }; } @@ -2628,13 +2636,7 @@ namespace Barotrauma public void BanPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } - GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true, rangeBan: false); - } - - public void BanPlayerRange(Client client) - { - if (GameMain.NetworkMember == null || client == null) { return; } - GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true, rangeBan: true); + GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true); } public override void AddToGUIUpdateList() @@ -2679,7 +2681,7 @@ namespace Barotrauma if (child.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { double voipAmplitude = 0.0f; - if (client.ID != GameMain.Client.ID) + if (client.SessionId != GameMain.Client.SessionId) { voipAmplitude = client.VoipSound?.CurrentAmplitude ?? 0.0f; } @@ -2750,25 +2752,24 @@ namespace Barotrauma if (GameMain.NetworkMember?.ServerSettings == null) { return; } PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; - if ((int)playStyle < 0 || - (int)playStyle >= ServerListScreen.PlayStyleBanners.Length) - { - return; - } - Sprite sprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; + Sprite sprite = GUIStyle + .GetComponentStyle($"PlayStyleBanner.{playStyle}")? + .GetSprite(GUIComponent.ComponentState.None); + if (sprite is null) { return; } + float scale = component.Rect.Width / sprite.size.X; sprite.Draw(spriteBatch, component.Center, scale: scale); if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) { var nameText = component.GetChild(); - nameText.Text = TextManager.Get("servertag." + playStyle); - nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.Text = TextManager.Get($"ServerTag.{playStyle}"); + nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); prevPlayStyle = playStyle; - component.ToolTip = TextManager.Get("servertagdescription." + playStyle); + component.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); } publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); @@ -3239,6 +3240,22 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } + foreach (var subElement in SubList.Content.Children) + { + subElement.CanBeFocused = true; + foreach (var textBlock in subElement.GetAllChildren()) + { + textBlock.Enabled = true; + } + } + + SubList.Content.RectTransform.SortChildren((rt1, rt2) => + { + SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo; + SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo; + return s1.Name.CompareTo(s2.Name); + }); + autoRestartBox.Parent.Visible = true; settingsBlocker.Visible = false; if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) @@ -3271,6 +3288,33 @@ namespace Barotrauma TextManager.Get("campaignstarting"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } } + + if (CampaignSetupUI != null) + { + foreach (var subElement in SubList.Content.Children) + { + var sub = subElement.UserData as SubmarineInfo; + bool tooExpensive = sub.Price > CampaignSetupUI.CurrentSettings.InitialMoney; + if (tooExpensive || !sub.IsCampaignCompatible) + { + foreach (var textBlock in subElement.GetAllChildren()) + { + textBlock.DisabledTextColor = (textBlock.UserData as string == "pricetext" && tooExpensive ? GUIStyle.Red : GUIStyle.TextColorNormal) * 0.7f; + textBlock.Enabled = false; + } + } + } + SubList.Content.RectTransform.SortChildren((rt1, rt2) => + { + SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo; + SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo; + int p1 = s1.Price; + if (!s1.IsCampaignCompatible) { p1 += 100000; } + int p2 = s2.Price; + if (!s2.IsCampaignCompatible) { p2 += 100000; } + return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); + }); + } } else { @@ -3672,7 +3716,7 @@ namespace Barotrauma } } - private List visibilityMenuOrder = new List(); + private readonly List visibilityMenuOrder = new List(); private void CreateSubmarineVisibilityMenu() { var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 356b8878c..cdce5489e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -55,9 +55,7 @@ namespace Barotrauma while (timer < duration) { GUI.ScreenOverlayColor = Color.Lerp(from, to, Math.Min(timer / duration, 1.0f)); - - timer += CoroutineManager.UnscaledDeltaTime; - + timer += CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs deleted file mode 100644 index 49bed19f7..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ /dev/null @@ -1,2186 +0,0 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Barotrauma.Networking; -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Barotrauma -{ - class ServerListScreen : Screen - { - //how often the client is allowed to refresh servers - private readonly TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); - - public ImmutableDictionary ContentPackagesByWorkshopId { get; private set; } - = ImmutableDictionary.Empty; - public ImmutableDictionary ContentPackagesByHash { get; private set; } - = ImmutableDictionary.Empty; - - private GUIFrame menu; - - private GUIListBox serverList; - private GUIFrame serverPreviewContainer; - private GUIListBox serverPreview; - - private GUIButton joinButton; - private ServerInfo selectedServer; - - private GUIButton scanServersButton; - - //friends list - private GUILayoutGroup friendsButtonHolder; - - private GUIButton friendsDropdownButton; - private GUIListBox friendsDropdown; - - private enum TernaryOption - { - Any, - Enabled, - Disabled - } - - private class FriendInfo - { - public UInt64 SteamID; - public string Name; - public Sprite Sprite; - public LocalizedString StatusText; - public bool PlayingThisGame; - public bool PlayingAnotherGame; - public string ConnectName; - public string ConnectEndpoint; - public UInt64 ConnectLobby; - - public bool InServer - { - get - { - return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); - } - } - } - private List friendsList; - private GUIFrame friendPopup; - private double friendsListUpdateTime; - - //favorite servers/history - private const string recentServersFile = "Data/recentservers.xml"; - private const string favoriteServersFile = "Data/favoriteservers.xml"; - private List favoriteServers; - private List recentServers; - - private readonly Dictionary activePings = new Dictionary(); - - private enum ServerListTab - { - All = 0, - Favorites = 1, - Recent = 2 - }; - private ServerListTab selectedTab; - private ServerListTab SelectedTab - { - get { return selectedTab; } - set - { - if (selectedTab == value) { return; } - var tabVals = Enum.GetValues(typeof(ServerListTab)); - for (int i = 0; i < tabVals.Length; i++) - { - tabButtons[i].Selected = false; - } - tabButtons[(int)value].Selected = true; - selectedTab = value; - FilterServers(); - } - } - private GUIButton[] tabButtons; - - private static Sprite[] playStyleBanners; - //server playstyle and tags - public static Sprite[] PlayStyleBanners - { - get - { - if (playStyleBanners == null) - { - LoadPlayStyleBanners(); - } - return playStyleBanners; - } - } - public static Color[] PlayStyleColors - { - get; private set; - } - - public GUITextBox ClientNameBox { get; private set; } - - public static Dictionary PlayStyleIcons - { - get; private set; - } - public static Dictionary PlayStyleIconColors - { - get; private set; - } - - private bool masterServerResponded; - private IRestResponse masterServerResponse; - - private readonly float[] columnRelativeWidth = new float[] { 0.1f, 0.1f, 0.7f, 0.12f, 0.08f, 0.08f }; - private readonly string[] columnLabel = new string[] { "ServerListCompatible", "ServerListHasPassword", "ServerListName", "ServerListRoundStarted", "ServerListPlayers", "ServerListPing" }; - - private GUILayoutGroup labelHolder; - private readonly List labelTexts = new List(); - - //filters - private GUITextBox searchBox; - private GUITickBox filterSameVersion; - private GUITickBox filterPassword; - private GUITickBox filterIncompatible; - private GUITickBox filterFull; - private GUITickBox filterEmpty; - private GUITickBox filterWhitelisted; - private Dictionary ternaryFilters; - private Dictionary filterTickBoxes; - private Dictionary playStyleTickBoxes; - private Dictionary gameModeTickBoxes; - private GUITickBox filterOffensive; - - //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. - private TernaryOption filterFriendlyFireValue = TernaryOption.Any; - private TernaryOption filterKarmaValue = TernaryOption.Any; - private TernaryOption filterTraitorValue = TernaryOption.Any; - private TernaryOption filterVoipValue = TernaryOption.Any; - private TernaryOption filterModdedValue = TernaryOption.Any; - - private string sortedBy; - - private GUIButton serverPreviewToggleButton; - - //a timer for preventing the client from spamming the refresh button faster than AllowedRefreshInterval - private DateTime refreshDisableTimer; - private bool waitingForRefresh; - - private bool steamPingInfoReady; - - private const float sidebarWidth = 0.2f; - public ServerListScreen() - { - GameMain.Instance.ResolutionChanged += CreateUI; - CreateUI(); - } - - private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) - { - var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) - { - Stretch = true - }; - - var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) - { - IsFixedSize = true, - }, null) - { - HoverColor = Color.Gray, - SelectedColor = Color.DarkGray, - CanBeFocused = false - }; - if (box.RectTransform.MinSize.Y > 0) - { - box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); - box.RectTransform.Resize(box.RectTransform.MinSize); - } - Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); - - var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) - { - UserData = TextManager.Get("servertag." + tag + ".label") - }; - GUIStyle.Apply(filterLabel, "GUITextBlock", null); - - var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); - dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); - dropDown.AddItem(TextManager.Get("servertag." + tag + ".true"), TernaryOption.Enabled, TextManager.Get("servertagdescription." + tag + ".true")); - dropDown.AddItem(TextManager.Get("servertag." + tag + ".false"), TernaryOption.Disabled, TextManager.Get("servertagdescription." + tag + ".false")); - dropDown.SelectItem(TernaryOption.Any); - dropDown.OnSelected = (_, data) => { - valueSetter((TernaryOption)data); - FilterServers(); - StoreServerFilters(); - return true; - }; - - ternaryFilters.Add(tag, dropDown); - } - - private void CreateUI() - { - menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); - - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center)) - { - RelativeSpacing = 0.02f, - Stretch = true - }; - - //------------------------------------------------------------------------------------- - //Top row - //------------------------------------------------------------------------------------- - - var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) - { - Padding = Vector4.Zero, - ForceUpperCase = ForceUpperCase.Yes, - AutoScaleHorizontal = true - }; - - var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; - - var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); - ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") - { - Text = MultiplayerPreferences.Instance.PlayerName, - MaxTextLength = Client.MaxNameLength, - OverflowClip = true - }; - - if (string.IsNullOrEmpty(ClientNameBox.Text)) - { - ClientNameBox.Text = SteamManager.GetUsername(); - } - ClientNameBox.OnTextChanged += (textbox, text) => - { - MultiplayerPreferences.Instance.PlayerName = text; - return true; - }; - - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); - - var tabVals = Enum.GetValues(typeof(ServerListTab)); - tabButtons = new GUIButton[tabVals.Length]; - foreach (ServerListTab tab in tabVals) - { - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabButtonHolder.RectTransform), - TextManager.Get("ServerListTab." + tab.ToString()), style: "GUITabButton") - { - OnClicked = (btn, usrdat) => - { - SelectedTab = tab; - return false; - } - }; - } - - var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") - { - IgnoreLayoutGroups = true - }; - - friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; - friendsList = new List(); - - //------------------------------------------------------------------------------------- - // Bottom row - //------------------------------------------------------------------------------------- - - var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y), - paddedFrame.RectTransform, Anchor.CenterRight)) - { - Stretch = true - }; - - var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - OutlineColor = Color.Black - }; - - GUILayoutGroup serverListContainer = null; - GUIFrame filtersHolder = null; - GUIButton filterToggle = null; - - void RecalculateHolder() - { - float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; - listContainerSubtract += serverPreviewContainer.Visible ? sidebarWidth : 0.0f; - - float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; - listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; - listContainerSubtract += serverPreviewContainer.Visible ? toggleButtonsSubtract : 0.0f; - - serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); - serverListHolder.Recalculate(); - } - - // filters ------------------------------------------- - - filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) - { - Color = new Color(12, 14, 15, 255) * 0.5f, - OutlineColor = Color.Black - }; - - float elementHeight = 0.05f; - var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) - { - Padding = Vector4.Zero, - AutoScaleHorizontal = true, - CanBeFocused = false - }; - - var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true }; - - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); - searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; - - var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft)) - { - ScrollBarVisible = true, - Spacing = (int)(5 * GUI.Scale) - }; - - filterToggle = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) - { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, - style: "UIToggleButton") - { - OnClicked = (btn, userdata) => - { - filtersHolder.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); - filtersHolder.Visible = !filtersHolder.Visible; - filtersHolder.IgnoreLayoutGroups = !filtersHolder.Visible; - - RecalculateHolder(); - - btn.Children.ForEach(c => c.SpriteEffects = !filtersHolder.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - return true; - } - }; - filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - - ternaryFilters = new Dictionary(); - filterTickBoxes = new Dictionary(); - - GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) - { - text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) - { - UserData = text, - Selected = defaultState, - ToolTip = addTooltip ? text : null, - OnSelected = (tickBox) => - { - FilterServers(); - StoreServerFilters(); - return true; - } - }; - filterTickBoxes.Add(key, tickBox); - return tickBox; - } - - filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); - filterPassword = addTickBox("FilterPassword".ToIdentifier()); - filterIncompatible = addTickBox("FilterIncompatibleServers".ToIdentifier()); - filterFull = addTickBox("FilterFullServers".ToIdentifier()); - filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); - filterWhitelisted = addTickBox("FilterWhitelistedServers".ToIdentifier()); - filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); - - // Filter Tags - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) - { - CanBeFocused = false - }; - - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); - - // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) - { - CanBeFocused = false - }; - - playStyleTickBoxes = new Dictionary(); - foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) - { - var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); - selectionTick.UserData = playStyle; - playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); - } - - // Game mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - - gameModeTickBoxes = new Dictionary(); - foreach (GameModePreset mode in GameModePreset.List) - { - if (mode.IsSinglePlayer) { continue; } - - var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); - selectionTick.UserData = mode.Identifier; - gameModeTickBoxes.Add(mode.Identifier, selectionTick); - } - - filters.Content.RectTransform.SizeChanged += () => - { - filters.Content.RectTransform.RecalculateChildren(true, true); - filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); - gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); - playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); - GUITextBlock.AutoScaleAndNormalize( - filterTickBoxes.Values.Select(tb => tb.TextBlock) - .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), - defaultScale: 1.0f); - if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) - { - filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); - filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f))); - } - }; - - // server list --------------------------------------------------------------------- - - serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; - - labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, - isHorizontal: true, childAnchor: Anchor.BottomLeft) - { - Stretch = true - }; - - for (int i = 0; i < columnRelativeWidth.Length; i++) - { - var btn = new GUIButton(new RectTransform(new Vector2(columnRelativeWidth[i], 1.0f), labelHolder.RectTransform), - text: TextManager.Get(columnLabel[i]), textAlignment: Alignment.Center, style: "GUIButtonSmall") - { - ToolTip = TextManager.Get(columnLabel[i]), - ForceUpperCase = ForceUpperCase.Yes, - UserData = columnLabel[i], - OnClicked = SortList - }; - 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, - UserData = "arrowup", - Visible = false - }; - new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) - { - CanBeFocused = false, - UserData = "arrowdown", - SpriteEffects = SpriteEffects.FlipVertically, - Visible = false - }; - } - - serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) - { - PlaySoundOnSelect = true, - ScrollBarVisible = true, - OnSelected = (btn, obj) => - { - if (obj is ServerInfo serverInfo) - { - joinButton.Enabled = true; - selectedServer = serverInfo; - if (!serverPreviewContainer.Visible) - { - serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); - serverPreviewToggleButton.Visible = true; - serverPreviewToggleButton.IgnoreLayoutGroups = false; - serverPreviewContainer.Visible = true; - serverPreviewContainer.IgnoreLayoutGroups = false; - RecalculateHolder(); - } - serverInfo.CreatePreviewWindow(serverPreview.Content); - serverPreview.ForceLayoutRecalculation(); - btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - } - return true; - } - }; - - //server preview panel -------------------------------------------------- - - serverPreviewToggleButton = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) - { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, - style: "UIToggleButton") - { - Visible = false, - OnClicked = (btn, userdata) => - { - serverPreviewContainer.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); - serverPreviewContainer.Visible = !serverPreviewContainer.Visible; - serverPreviewContainer.IgnoreLayoutGroups = !serverPreviewContainer.Visible; - - RecalculateHolder(); - - btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - return true; - } - }; - - serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) - { - Color = new Color(12, 14, 15, 255) * 0.5f, - OutlineColor = Color.Black, - IgnoreLayoutGroups = true, - Visible = false - }; - serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) - { - Padding = Vector4.One * 10 * GUI.Scale, - HoverCursor = CursorState.Default, - OnSelected = (component, o) => false - }; - - // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.02f, - Stretch = true - }; - - GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Back")) - { - OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu - }; - - scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("ServerListRefresh")) - { - OnClicked = (btn, userdata) => { RefreshServers(); return true; } - }; - - var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("serverlistdirectjoin")) - { - OnClicked = (btn, userdata) => - { - if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) - { - ClientNameBox.Flash(); - ClientNameBox.Select(); - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - return false; - } - ShowDirectJoinPrompt(); - return true; - } - }; - - joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("ServerListJoin")) - { - OnClicked = (btn, userdata) => - { - if (selectedServer != null) - { - if (!string.IsNullOrWhiteSpace(selectedServer.IP) && !string.IsNullOrWhiteSpace(selectedServer.Port) && int.TryParse(selectedServer.Port, out _)) - { - JoinServer(selectedServer.IP + ":" + selectedServer.Port, selectedServer.ServerName); - } - else if (selectedServer.LobbyID != 0) - { - Steam.SteamManager.JoinLobby(selectedServer.LobbyID, true); - } - else - { - new GUIMessageBox("", TextManager.Get("ServerOffline")); - return false; - } - } - return true; - }, - Enabled = false - }; - - buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f)); - - //-------------------------------------------------------- - - bottomRow.Recalculate(); - serverListHolder.Recalculate(); - serverListContainer.Recalculate(); - labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); - labelHolder.Recalculate(); - - serverList.Content.RectTransform.SizeChanged += () => - { - labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); - labelHolder.Recalculate(); - foreach (GUITextBlock labelText in labelTexts) - { - labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); - } - RecalculateHolder(); - }; - - button.SelectedColor = button.Color; - refreshDisableTimer = DateTime.Now; - - //recent and favorite servers - ReadServerMemFromFile(recentServersFile, ref recentServers); - ReadServerMemFromFile(favoriteServersFile, ref favoriteServers); - recentServers.ForEach(s => s.Recent = true); - favoriteServers.ForEach(s => s.Favorite = true); - - SelectedTab = ServerListTab.All; - tabButtons[(int)selectedTab].Selected = true; - - RecalculateHolder(); - } - - - private static void LoadPlayStyleBanners() - { - //playstyle banners - playStyleBanners = new Sprite[Enum.GetValues(typeof(PlayStyle)).Length]; - PlayStyleColors = new Color[Enum.GetValues(typeof(PlayStyle)).Length]; - PlayStyleIcons = new Dictionary(); - PlayStyleIconColors = new Dictionary(); - - XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); - - var rootElement = playStylesDoc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); - foreach (var element in rootElement.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "playstylebanner": - if (Enum.TryParse(element.GetAttributeString("identifier", ""), out PlayStyle playStyle)) - { - PlayStyleBanners[(int)playStyle] = new Sprite(element, lazyLoad: true); - PlayStyleColors[(int)playStyle] = element.GetAttributeColor("color", Color.White); - } - break; - case "playstyleicon": - string identifier = element.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(identifier)) { continue; } - PlayStyleIcons[identifier] = new Sprite(element, lazyLoad: true); - PlayStyleIconColors[identifier] = element.GetAttributeColor("color", Color.White); - break; - } - } - } - - private void ReadServerMemFromFile(string file, ref List servers) - { - if (servers == null) { servers = new List(); } - servers.Clear(); - - if (!File.Exists(file)) { return; } - - XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null) - { - DebugConsole.NewMessage("Failed to load file \"" + file + "\". Attempting to recreate the file..."); - try - { - doc = new XDocument(new XElement("servers")); - doc.Save(file); - DebugConsole.NewMessage("Recreated \"" + file + "\"."); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to recreate the file \"" + file + "\".", e); - } - return; - } - - bool saveCleanup = false; - foreach (XElement element in doc.Root.Elements()) - { - if (element.Name != "ServerInfo") { continue; } - var info = ServerInfo.FromXElement(element); - if (!servers.Any(s => s.Equals(info))) - { - servers.Add(info); - } - else - { - saveCleanup = true; - } - } - if (saveCleanup) { WriteServerMemToFile(file, servers); } - } - - private void WriteServerMemToFile(string file, List servers) - { - if (servers == null) { return; } - - XDocument doc = new XDocument(); - XElement rootElement = new XElement("servers"); - doc.Add(rootElement); - - foreach (ServerInfo info in servers) - { - rootElement.Add(info.ToXElement()); - } - - doc.SaveSafe(file); - } - - public ServerInfo UpdateServerInfoWithServerSettings(NetworkConnection endpoint, ServerSettings serverSettings) - { - UInt64 steamId = 0; - string ip = ""; string port = ""; - if (endpoint is SteamP2PConnection steamP2PConnection) { steamId = steamP2PConnection.SteamID; } - else if (endpoint is LidgrenConnection lidgrenConnection) - { - ip = lidgrenConnection.IPString; - port = lidgrenConnection.Port.ToString(); - } - - bool isInfoNew = false; - ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && - (steamId != 0 ? steamId == serverInfo.OwnerID : (ip == serverInfo.IP && port == serverInfo.Port)))?.UserData as ServerInfo; - if (info == null) - { - isInfoNew = true; - info = new ServerInfo(); - } - - info.ServerName = serverSettings.ServerName; - info.ServerMessage = serverSettings.ServerMessageText; - info.OwnerID = steamId; - info.LobbyID = SteamManager.CurrentLobbyID; - info.IP = ip; - info.Port = port; - info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty; - info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; - info.GameVersion = GameMain.Version.ToString(); - info.MaxPlayers = serverSettings.MaxPlayers; - info.PlayStyle = serverSettings.PlayStyle; - info.RespondedToSteamQuery = true; - info.UsingWhiteList = serverSettings.Whitelist.Enabled; - info.TraitorsEnabled = serverSettings.TraitorsEnabled; - info.SubSelectionMode = serverSettings.SubSelectionMode; - info.ModeSelectionMode = serverSettings.ModeSelectionMode; - info.VoipEnabled = serverSettings.VoiceChatEnabled; - info.FriendlyFireEnabled = serverSettings.AllowFriendlyFire; - info.KarmaEnabled = serverSettings.KarmaEnabled; - info.PlayerCount = GameMain.Client.ConnectedClients.Count; - info.PingChecked = false; - info.HasPassword = serverSettings.HasPassword; - info.OwnerVerified = true; - - if (isInfoNew) - { - AddToServerList(info); - } - - return info; - } - - public void AddToRecentServers(ServerInfo info) - { - if (!string.IsNullOrEmpty(info.IP)) - { - //don't add localhost to recent servers - if (IPAddress.TryParse(info.IP, out IPAddress ip) && IPAddress.IsLoopback(ip)) { return; } - } - - info.Recent = true; - ServerInfo existingInfo = recentServers.Find(info.MatchesByEndpoint); - if (existingInfo == null) - { - recentServers.Add(info); - } - else - { - int index = recentServers.IndexOf(existingInfo); - recentServers[index] = info; - } - - WriteServerMemToFile(recentServersFile, recentServers); - } - - public bool IsFavorite(ServerInfo info) - { - return favoriteServers.Any(info.MatchesByEndpoint); - } - - public void AddToFavoriteServers(ServerInfo info) - { - info.Favorite = true; - ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); - if (existingInfo == null) - { - favoriteServers.Add(info); - } - else - { - int index = favoriteServers.IndexOf(existingInfo); - favoriteServers[index] = info; - } - - WriteServerMemToFile(favoriteServersFile, favoriteServers); - } - - public void RemoveFromFavoriteServers(ServerInfo info) - { - info.Favorite = false; - ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); - if (existingInfo != null) - { - favoriteServers.Remove(existingInfo); - WriteServerMemToFile(favoriteServersFile, favoriteServers); - } - } - - private bool SortList(GUIButton button, object obj) - { - if (!(obj is string sortBy)) { return false; } - SortList(sortBy, toggle: true); - return true; - } - - private void SortList(string sortBy, bool toggle) - { - GUIButton button = labelHolder.GetChildByUserData(sortBy) as GUIButton; - if (button == null) { return; } - - sortedBy = sortBy; - - var arrowUp = button.GetChildByUserData("arrowup"); - var arrowDown = button.GetChildByUserData("arrowdown"); - - //disable arrow buttons in other labels - foreach (var child in button.Parent.Children) - { - if (child != button) - { - child.GetChildByUserData("arrowup").Visible = false; - child.GetChildByUserData("arrowdown").Visible = false; - } - } - - bool ascending = arrowUp.Visible; - if (toggle) - { - ascending = !ascending; - } - - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; - serverList.Content.RectTransform.SortChildren((c1, c2) => - { - ServerInfo s1 = c1.GUIComponent.UserData as ServerInfo; - ServerInfo s2 = c2.GUIComponent.UserData as ServerInfo; - - if (s1 == null && s2 == null) - { - return 0; - } - else if (s1 == null) - { - return ascending ? 1 : -1; - } - else if (s2 == null) - { - return ascending ? -1 : 1; - } - - switch (sortBy) - { - case "ServerListCompatible": - bool? s1Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s1.GameVersion); - bool? s2Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s2.GameVersion); - - //convert to int to make sorting easier - //1 Compatible - //0 Unknown - //-1 Incompatible - int s1CompatibleInt = s1Compatible.HasValue ? - (s1Compatible.Value ? 1 : -1) : - 0; - int s2CompatibleInt = s2Compatible.HasValue ? - (s2Compatible.Value ? 1 : -1) : - 0; - return s2CompatibleInt.CompareTo(s1CompatibleInt) * (ascending ? 1 : -1); - case "ServerListHasPassword": - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case "ServerListName": - return string.Compare(s1.ServerName, s2.ServerName) * (ascending ? 1 : -1); - case "ServerListRoundStarted": - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case "ServerListPlayers": - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case "ServerListPing": - return s2.Ping.CompareTo(s1.Ping) * (ascending ? 1 : -1); - default: - return 0; - } - }); - } - - public override void Select() - { - base.Select(); - - ContentPackagesByWorkshopId = ContentPackageManager.AllPackages - .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) - .Where(p => p.Key != 0) - .GroupBy(x => x.Key).Select(g => g.First()) - .ToImmutableDictionary(); - ContentPackagesByHash = ContentPackageManager.AllPackages - .Select(p => new KeyValuePair(p.Hash.StringRepresentation, p)) - .GroupBy(x => x.Key).Select(g => g.First()) - .ToImmutableDictionary(); - - SelectedTab = ServerListTab.All; - GameMain.ServerListScreen.LoadServerFilters(); - if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) - { - var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => - { - filterOffensive.Selected = true; - filterOffensivePrompt.Close(); - return true; - }; - filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; - - var config = GameSettings.CurrentConfig; - config.ShowOffensiveServerPrompt = false; - GameSettings.SetCurrentConfig(config); - } - - Steamworks.SteamMatchmaking.ResetActions(); - - if (GameMain.Client != null) - { - GameMain.Client.Disconnect(); - GameMain.Client = null; - } - - RefreshServers(); - } - - public override void Deselect() - { - ContentPackagesByWorkshopId = ImmutableDictionary.Empty; - ContentPackagesByHash = ImmutableDictionary.Empty; - base.Deselect(); - - GameSettings.SaveCurrentConfig(); - } - - public override void Update(double deltaTime) - { - base.Update(deltaTime); - - UpdateFriendsList(); - UpdateInfoQueries(); - - if (PlayerInput.PrimaryMouseButtonClicked()) - { - friendPopup = null; - if (friendsDropdown != null && friendsDropdownButton != null && - !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && - !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) - { - friendsDropdown.Visible = false; - } - } - } - - private void FilterServers() - { - serverList.Content.RemoveChild(serverList.Content.FindChild("noresults")); - - foreach (GUIComponent child in serverList.Content.Children) - { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } - - Version remoteVersion = null; - if (!string.IsNullOrEmpty(serverInfo.GameVersion)) - { - Version.TryParse(serverInfo.GameVersion, out remoteVersion); - } - - //never show newer versions - //(ignore revision number, it doesn't affect compatibility) - if (remoteVersion != null && - ToolBox.VersionNewerIgnoreRevision(GameMain.Version, remoteVersion)) - { - child.Visible = false; - } - else - { - bool incompatible = - remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion); - - var karmaFilterPassed = filterKarmaValue == TernaryOption.Any|| (filterKarmaValue == TernaryOption.Enabled) == serverInfo.KarmaEnabled; - var friendlyFireFilterPassed = filterFriendlyFireValue == TernaryOption.Any || (filterFriendlyFireValue == TernaryOption.Enabled) == serverInfo.FriendlyFireEnabled; - var traitorsFilterPassed = filterTraitorValue == TernaryOption.Any || (filterTraitorValue == TernaryOption.Enabled) == (serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe); - var voipFilterPassed = filterVoipValue == TernaryOption.Any || (filterVoipValue == TernaryOption.Enabled) == serverInfo.VoipEnabled; - var moddedFilterPassed = filterModdedValue == TernaryOption.Any || (filterModdedValue == TernaryOption.Enabled) == serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true")); - - child.Visible = - serverInfo.OwnerVerified && - serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && - (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && - (!filterPassword.Selected || !serverInfo.HasPassword) && - (!filterIncompatible.Selected || !incompatible) && - (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && - (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && - (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && - (!filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && - karmaFilterPassed && - friendlyFireFilterPassed && - traitorsFilterPassed && - voipFilterPassed && - moddedFilterPassed && - ((selectedTab == ServerListTab.All && (serverInfo.LobbyID != 0 || !string.IsNullOrWhiteSpace(serverInfo.Port))) || - (selectedTab == ServerListTab.Recent && serverInfo.Recent) || - (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); - } - - foreach (GUITickBox tickBox in playStyleTickBoxes.Values) - { - var playStyle = (PlayStyle)tickBox.UserData; - if (!tickBox.Selected && (serverInfo.PlayStyle == playStyle || !serverInfo.PlayStyle.HasValue)) - { - child.Visible = false; - break; - } - } - - foreach (GUITickBox tickBox in gameModeTickBoxes.Values) - { - var gameMode = (Identifier)tickBox.UserData; - if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode == gameMode) - { - child.Visible = false; - break; - } - } - } - - if (serverList.Content.Children.All(c => !c.Visible)) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverList.Content.RectTransform), - TextManager.Get("NoMatchingServers")) - { - UserData = "noresults" - }; - } - - serverList.UpdateScrollBarSize(); - } - - private Queue pendingQueries = new Queue(); - int activeQueries = 0; - private void QueueInfoQuery(ServerInfo info) - { - pendingQueries.Enqueue(info); - } - - private void OnQueryDone(ServerInfo info) - { - activeQueries--; - } - - public void UpdateInfoQueries() - { - while (activeQueries < 25 && pendingQueries.Count > 0) - { - activeQueries++; - var info = pendingQueries.Dequeue(); - info.QueryLiveInfo(UpdateServerInfo, OnQueryDone); - } - } - - private void ShowDirectJoinPrompt() - { - var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", - new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); - msgBox.Content.ChildAnchor = Anchor.TopCenter; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter) - { - IgnoreLayoutGroups = false, - Stretch = true, - RelativeSpacing = 0.05f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); - var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); - - content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); - content.RectTransform.IsFixedSize = true; - msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f)); - - var okButton = msgBox.Buttons[0]; - okButton.Enabled = false; - okButton.OnClicked = (btn, userdata) => - { - JoinServer(endpointBox.Text, ""); - msgBox.Close(); - return true; - }; - - var favoriteButton = msgBox.Buttons[1]; - favoriteButton.Enabled = false; - favoriteButton.OnClicked = (button, userdata) => - { - UInt64 steamId = SteamManager.SteamIDStringToUInt64(endpointBox.Text); - string ip = ""; int port = 0; - if (steamId == 0) - { - string hostIP = endpointBox.Text; - - string[] address = hostIP.Split(':'); - if (address.Length == 1) - { - ip = hostIP; - port = NetConfig.DefaultPort; - } - else - { - ip = string.Join(":", address.Take(address.Length - 1)); - if (!int.TryParse(address[address.Length - 1], out port)) - { - DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); - port = NetConfig.DefaultPort; - } - } - } - - //TODO: add a better way to get the query port, right now we're just assuming that it'll always be the default - ServerInfo serverInfo = new ServerInfo() - { - ServerName = "Server", - OwnerID = steamId, - IP = ip, - Port = port.ToString(), - QueryPort = NetConfig.DefaultQueryPort.ToString(), - GameVersion = GameMain.Version.ToString(), - PlayStyle = null - }; - - var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - info.MatchesByEndpoint(serverInfo)); - - if (serverFrame != null) - { - serverInfo = serverFrame.UserData as ServerInfo; - } - else - { - AddToServerList(serverInfo); - } - - AddToFavoriteServers(serverInfo); - - SelectedTab = ServerListTab.Favorites; - FilterServers(); - - QueueInfoQuery(serverInfo); - - msgBox.Close(); - return false; - }; - - var cancelButton = msgBox.Buttons[2]; - cancelButton.OnClicked = msgBox.Close; - - endpointBox.OnTextChanged += (textBox, text) => - { - okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); - return true; - }; - } - - private bool JoinFriend(GUIButton button, object userdata) - { - FriendInfo info = userdata as FriendInfo; - - if (info.InServer) - { - if (info.ConnectLobby != 0) - { - GameMain.Instance.ConnectLobby = info.ConnectLobby; - GameMain.Instance.ConnectEndpoint = null; - GameMain.Instance.ConnectName = null; - } - else - { - GameMain.Instance.ConnectLobby = 0; - GameMain.Instance.ConnectEndpoint = info.ConnectEndpoint; - GameMain.Instance.ConnectName = info.ConnectName; - } - } - return false; - } - - private bool OpenFriendPopup(GUIButton button, object userdata) - { - FriendInfo info = userdata as FriendInfo; - - if (info.InServer) - { - int framePadding = 5; - - friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); - - var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), info.ConnectName ?? "[Unnamed]"); - serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) - { - UserData = info - }; - joinButton.OnClicked = JoinFriend; - joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); - int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; - int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); - - // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. - Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); - - // Add padding to the server name to match the padding on the button text. - serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); - - // Get the dimensions of the text we want to show, plus the extra padding we added. - Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); - - // Now determine how large the parent frame has to be to exactly fit our two controls. - Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); - - var popupPos = PlayerInput.MousePosition.ToPoint(); - if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) - { - // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. - popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; - } - - // Apply the size and position changes. - friendPopup.RectTransform.NonScaledSize = frameDims; - friendPopup.RectTransform.RelativeOffset = Vector2.Zero; - friendPopup.RectTransform.AbsoluteOffset = popupPos; - - joinButton.RectTransform.NonScaledSize = finalButtonSize; - - friendPopup.RectTransform.RecalculateChildren(true); - friendPopup.RectTransform.SetPosition(Anchor.TopLeft); - } - - return false; - } - - private enum AvatarSize - { - Small, - Medium, - Large - } - - private void UpdateFriendsList() - { - if (!SteamManager.IsInitialized) { return; } - - if (friendsListUpdateTime > Timing.TotalTime) { return; } - friendsListUpdateTime = Timing.TotalTime + 5.0; - - float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; - - if (friendsDropdown == null) - { - friendsDropdown = new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) - { - OutlineColor = Color.Black, - Visible = false - }; - } - friendsDropdown.ClearChildren(); - - AvatarSize avatarSize = AvatarSize.Large; - if (friendsButtonHolder.RectTransform.Rect.Height <= 24) - { - avatarSize = AvatarSize.Small; - } - else if (friendsButtonHolder.RectTransform.Rect.Height <= 48) - { - avatarSize = AvatarSize.Medium; - } - - List friends = Steamworks.SteamFriends.GetFriends().ToList(); - - for (int i = friendsList.Count - 1; i >= 0; i--) - { - var friend = friendsList[i]; - if (!friends.Any(g => g.Id == friend.SteamID && g.IsOnline)) - { - friend.Sprite?.Remove(); - friendsList.RemoveAt(i); - } - } - - foreach (var friend in friends) - { - if (!friend.IsOnline) { continue; } - - FriendInfo info = friendsList.Find(f => f.SteamID == friend.Id); - if (info == null) - { - info = new FriendInfo() - { - SteamID = friend.Id - }; - friendsList.Insert(0, info); - } - - if (info.Sprite == null) - { - Func> avatarFunc = null; - switch (avatarSize) - { - case AvatarSize.Small: - avatarFunc = Steamworks.SteamFriends.GetSmallAvatarAsync; - break; - case AvatarSize.Medium: - avatarFunc = Steamworks.SteamFriends.GetMediumAvatarAsync; - break; - case AvatarSize.Large: - avatarFunc = Steamworks.SteamFriends.GetLargeAvatarAsync; - break; - } - TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(friend.Id), (task) => - { - if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } - if (!img.HasValue) { return; } - - var avatarImage = img.Value; - - const int desaturatedWeight = 180; - - byte[] avatarData = (byte[])avatarImage.Data.Clone(); - for (int i = 0; i < avatarData.Length; i += 4) - { - int luma = (avatarData[i + 0] * 299 + avatarData[i + 1] * 587 + avatarData[i + 2] * 114) / 1000; - luma = (int)(luma * 0.7f + ((luma / 100.0f) * (luma / 255.0f) * 255.0f * 0.3f)); - int chn0 = ((avatarData[i + 0] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn1 = ((avatarData[i + 1] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn2 = ((avatarData[i + 2] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn3 = 255; - - chn0 = chn0 * chn3 / 255; - chn1 = chn1 * chn3 / 255; - chn2 = chn2 * chn3 / 255; - - avatarData[i + 0] = chn0 > 255 ? (byte)255 : (byte)chn0; - avatarData[i + 1] = chn1 > 255 ? (byte)255 : (byte)chn1; - avatarData[i + 2] = chn2 > 255 ? (byte)255 : (byte)chn2; - avatarData[i + 3] = chn3 > 255 ? (byte)255 : (byte)chn3; - } - CrossThread.RequestExecutionOnMainThread(() => - { - //TODO: create an avatar atlas? - var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); - avatarTexture.SetData(avatarData); - info.Sprite = new Sprite(avatarTexture, null, null); - }); - }); - } - - info.Name = friend.Name; - - info.ConnectName = null; - info.ConnectEndpoint = null; - info.ConnectLobby = 0; - - info.PlayingThisGame = friend.IsPlayingThisGame; - info.PlayingAnotherGame = friend.GameInfo.HasValue; - - if (friend.IsPlayingThisGame) - { - info.StatusText = friend.GetRichPresence("status") ?? ""; - string connectCommand = friend.GetRichPresence("connect") ?? ""; - - try - { - ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand), out info.ConnectName, out info.ConnectEndpoint, out info.ConnectLobby); - } - catch (IndexOutOfRangeException e) - { -#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.CleanupStackTrace()); -#endif - info.ConnectName = null; - info.ConnectEndpoint = null; - info.ConnectLobby = 0; - } - } - else - { - info.StatusText = TextManager.Get(info.PlayingAnotherGame ? "FriendPlayingAnotherGame" : "FriendNotPlaying"); - } - } - - friendsList.Sort((a, b) => - { - if (a.InServer && !b.InServer) { return -1; } - if (b.InServer && !a.InServer) { return 1; } - if (a.PlayingThisGame && !b.PlayingThisGame) { return -1; } - if (b.PlayingThisGame && !a.PlayingThisGame) { return 1; } - return 0; - }); - - friendsButtonHolder.ClearChildren(); - - if (friendsList.Count > 0) - { - friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") - { - OnClicked = (button, udt) => - { - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - friendsDropdown.Visible = !friendsDropdown.Visible; - return false; - } - }; - } - else - { - friendsDropdownButton = null; - friendsDropdown.Visible = false; - } - - int buttonCount = 0; - - for (int i = 0; i < friendsList.Count; i++) - { - var friend = friendsList[i]; - buttonCount++; - - if (buttonCount <= 5) - { - string style = "GUIButtonFriendNotPlaying"; - if (friend.InServer) - { - style = "GUIButtonFriendPlaying"; - } - else - { - style = friend.PlayingThisGame ? "GUIButtonFriendPlaying" : "GUIButtonFriendNotPlaying"; - } - - var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) - { - UserData = friend, - OnClicked = OpenFriendPopup - }; - guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; - - if (friend.Sprite != null) - { - static Color BrightenColor(Color color) - { - Vector3 hls = ToolBox.RgbToHLS(color); - hls.Y = hls.Y * 0.3f + 0.7f; - hls.Z = hls.Z * 0.6f + 0.4f; - - return ToolBox.HLSToRGB(hls); - } - - var imgColor = BrightenColor(guiButton.Color); - var imgHoverColor = BrightenColor(guiButton.HoverColor); - var imgSelectColor = BrightenColor(guiButton.SelectedColor); - var imgPressColor = BrightenColor(guiButton.PressedColor); - var guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) - { - Color = imgColor, - HoverColor = imgHoverColor, - SelectedColor = imgSelectColor, - PressedColor = imgPressColor, - CanBeFocused = false - }; - guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) - { - Color = Color.White * 0.8f, - HoverColor = Color.White * 0.8f, - SelectedColor = Color.White * 0.8f, - PressedColor = Color.White * 0.8f, - BlendState = BlendState.Additive, - CanBeFocused = false - }; - } - } - - var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); - var guiImage2TheSequel = new GUIImage(new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) } , friend.Sprite, null, true); - - var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) - { - Font = GUIStyle.SmallFont - }; - if (friend.PlayingThisGame) { textBlock.TextColor = GUIStyle.Green; } - if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } - - if (friend.InServer) - { - var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") - { - UserData = friend - }; - joinButton.OnClicked = JoinFriend; - } - } - - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - - friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; - } - - private void RefreshServers() - { - if (waitingForRefresh) { return; } - - steamPingInfoReady = false; - - CoroutineManager.StopCoroutines("EstimateLobbyPing"); - - if (SteamManager.IsInitialized) - { - TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => - { - steamPingInfoReady = true; - }); - } - - friendsListUpdateTime = Timing.TotalTime - 1.0; - UpdateFriendsList(); - - serverList.ClearChildren(); - serverPreview.Content.ClearChildren(); - joinButton.Enabled = false; - selectedServer = null; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), - TextManager.Get("RefreshingServerList"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - - CoroutineManager.StartCoroutine(WaitForRefresh()); - } - - private IEnumerable WaitForRefresh() - { - waitingForRefresh = true; - if (refreshDisableTimer > DateTime.Now) - { - yield return new WaitForSeconds((float)(refreshDisableTimer - DateTime.Now).TotalSeconds); - } - - recentServers.Concat(favoriteServers).ForEach(si => si.OwnerVerified = false); - if (GameSettings.CurrentConfig.UseSteamMatchmaking) - { - serverList.ClearChildren(); - if (!SteamManager.GetServers(AddToServerList, ServerQueryFinished)) - { - serverList.ClearChildren(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), - TextManager.Get("ServerListNoSteamConnection"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - scanServersButton.Enabled = false; - } - else - { - List knownServers = recentServers.Concat(favoriteServers).ToList(); - foreach (ServerInfo info in knownServers) - { - AddToServerList(info); - QueueInfoQuery(info); - } - scanServersButton.Enabled = true; - } - } - - refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; - - yield return CoroutineStatus.Success; - } - - private void UpdateServerList(string masterServerData) - { - serverList.ClearChildren(); - - if (masterServerData.Substring(0, 5).Equals("error", StringComparison.OrdinalIgnoreCase)) - { - DebugConsole.ThrowError("Error while connecting to master server (" + masterServerData + ")!"); - return; - } - - string[] lines = masterServerData.Split('\n'); - List serverInfos = new List(); - for (int i = 0; i < lines.Length; i++) - { - string[] arguments = lines[i].Split('|'); - if (arguments.Length < 3) continue; - - string ip = arguments[0]; - string port = arguments[1]; - string serverName = arguments[2]; - bool gameStarted = arguments.Length > 3 && arguments[3] == "1"; - string currPlayersStr = arguments.Length > 4 ? arguments[4] : ""; - string maxPlayersStr = arguments.Length > 5 ? arguments[5] : ""; - bool hasPassWord = arguments.Length > 6 && arguments[6] == "1"; - string gameVersion = arguments.Length > 7 ? arguments[7] : ""; - string contentPackageNames = arguments.Length > 8 ? arguments[8] : ""; - string contentPackageHashes = arguments.Length > 9 ? arguments[9] : ""; - - int.TryParse(currPlayersStr, out int playerCount); - int.TryParse(maxPlayersStr, out int maxPlayers); - - var serverInfo = new ServerInfo() - { - IP = ip, - Port = port, - ServerName = serverName, - GameStarted = gameStarted, - PlayerCount = playerCount, - MaxPlayers = maxPlayers, - HasPassword = hasPassWord, - GameVersion = gameVersion, - OwnerVerified = true - }; - foreach (string contentPackageName in contentPackageNames.Split(',')) - { - if (string.IsNullOrEmpty(contentPackageName)) continue; - serverInfo.ContentPackageNames.Add(contentPackageName); - } - foreach (string contentPackageHash in contentPackageHashes.Split(',')) - { - if (string.IsNullOrEmpty(contentPackageHash)) continue; - serverInfo.ContentPackageHashes.Add(contentPackageHash); - } - - serverInfos.Add(serverInfo); - } - - serverList.Content.ClearChildren(); - if (serverInfos.Count() == 0) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), - TextManager.Get("NoServers"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - return; - } - foreach (ServerInfo serverInfo in serverInfos) - { - AddToServerList(serverInfo); - } - } - - private void AddToServerList(ServerInfo serverInfo) - { - var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - (info.LobbyID == serverInfo.LobbyID || - (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && - serverInfo.OwnerVerified)) && - (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); - - if (serverFrame == null) - { - serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, - style: "ListBoxElement") - { - UserData = serverInfo - }; - new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - //RelativeSpacing = 0.02f - }; - } - else - { - int index = recentServers.IndexOf(serverFrame.UserData as ServerInfo); - if (index >= 0) - { - recentServers[index] = serverInfo; - serverInfo.Recent = true; - } - index = favoriteServers.IndexOf(serverFrame.UserData as ServerInfo); - if (index >= 0) - { - favoriteServers[index] = serverInfo; - serverInfo.Favorite = true; - } - } - serverFrame.UserData = serverInfo; - - if (serverInfo.OwnerVerified) - { - var childrenToRemove = serverList.Content.FindChildren(c => (c.UserData is ServerInfo info) && info != serverInfo && - (serverInfo.OwnerID != 0 ? info.OwnerID == serverInfo.OwnerID : info.IP == serverInfo.IP)).ToList(); - foreach (var child in childrenToRemove) - { - serverList.Content.RemoveChild(child); - } - } - - UpdateServerInfo(serverInfo); - - SortList(sortedBy, toggle: false); - FilterServers(); - } - - private void UpdateServerInfo(ServerInfo serverInfo) - { - var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - (info.LobbyID == serverInfo.LobbyID || - (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && - serverInfo.OwnerVerified)) && - (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); - if (serverFrame == null) return; - - var serverContent = serverFrame.Children.First() as GUILayoutGroup; - serverContent.ClearChildren(); - - var compatibleBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[0], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") - { - CanBeFocused = false, - Selected = - (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true), - UserData = "compatible" - }; - - var passwordBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[1], 0.5f), serverContent.RectTransform, Anchor.Center), label: "", style: "GUIServerListPasswordTickBox") - { - ToolTip = TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword"), - Selected = serverInfo.HasPassword, - CanBeFocused = false, - UserData = "password" - }; - - var serverName = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[2] * 1.1f, 1.0f), serverContent.RectTransform), -#if !DEBUG - serverInfo.ServerName, -#else - ((serverInfo.OwnerID != 0 || serverInfo.LobbyID != 0) ? "[STEAMP2P] " : "[LIDGREN] ") + serverInfo.ServerName, -#endif - style: "GUIServerListTextBox"); - serverName.UserData = serverName.Text; - serverName.RectTransform.SizeChanged += () => - { - serverName.Text = ToolBox.LimitString(serverName.Text, serverName.Font, serverName.Rect.Width); - }; - - if (serverInfo.ContentPackageNames.Any()) - { - if (serverInfo.ContentPackageNames.Any(p => !GameMain.VanillaContent.NameMatches(p))) - { - serverName.TextColor = GUIStyle.ModdedServerColor; - } - } - - new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[3], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") - { - ToolTip = TextManager.Get((serverInfo.GameStarted) ? "ServerListRoundStarted" : "ServerListRoundNotStarted"), - Selected = serverInfo.GameStarted, - CanBeFocused = false - }; - - var serverPlayers = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[4], 1.0f), serverContent.RectTransform), - serverInfo.PlayerCount + "/" + serverInfo.MaxPlayers, style: "GUIServerListTextBox", textAlignment: Alignment.Right) - { - ToolTip = TextManager.Get("ServerListPlayers") - }; - - var serverPingText = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[5], 1.0f), serverContent.RectTransform), "?", - style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) - { - ToolTip = TextManager.Get("ServerListPing") - }; - - if (serverInfo.PingChecked) - { - serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; - serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); - } - else if (!string.IsNullOrEmpty(serverInfo.IP)) - { - try - { - GetServerPing(serverInfo, serverPingText); - } - catch (NullReferenceException ex) - { - DebugConsole.ThrowError("Ping is null", ex); - } - } - else if (serverInfo.PingLocation != null) - { - CoroutineManager.StartCoroutine(EstimateLobbyPing(serverInfo, serverPingText), "EstimateLobbyPing"); - } - - if (serverInfo.LobbyID == 0 && (string.IsNullOrWhiteSpace(serverInfo.IP) || string.IsNullOrWhiteSpace(serverInfo.Port))) - { - LocalizedString toolTip = TextManager.Get("ServerOffline"); - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - serverName.TextColor *= 0.8f; - serverPlayers.TextColor *= 0.8f; - } - else if (GameSettings.CurrentConfig.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) - { - LocalizedString toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); - compatibleBox.Selected = false; - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - serverName.TextColor *= 0.8f; - serverPlayers.TextColor *= 0.8f; - } - else if (string.IsNullOrEmpty(serverInfo.GameVersion) || !serverInfo.ContentPackageHashes.Any()) - { - compatibleBox.Selected = false; - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) - { - ToolTip = TextManager.Get(string.IsNullOrEmpty(serverInfo.GameVersion) ? - "ServerListUnknownVersion" : - "ServerListUnknownContentPackage") - }; - } - else if (!compatibleBox.Selected) - { - LocalizedString toolTip = ""; - if (serverInfo.GameVersion != GameMain.Version.ToString()) - { - toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); - } - - int maxIncompatibleToList = 10; - List incompatibleModNames = new List(); - for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) - { - bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); - if (listAsIncompatible) - { - incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", - ("[name]", serverInfo.ContentPackageNames[i]), - ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i])))); - - } - } - if (incompatibleModNames.Any()) - { - toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); - if (incompatibleModNames.Count > maxIncompatibleToList) - { - toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); - } - } - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - - serverName.TextColor *= 0.5f; - serverPlayers.TextColor *= 0.5f; - } - else - { - LocalizedString toolTip = ""; - for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) - { - if (!ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) - { - if (toolTip != "") { toolTip += "\n"; } - toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); - break; - } - } - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - } - - serverContent.Recalculate(); - - if (serverInfo.Favorite) - { - AddToFavoriteServers(serverInfo); - } - - SortList(sortedBy, toggle: false); - FilterServers(); - } - - private IEnumerable EstimateLobbyPing(ServerInfo serverInfo, GUITextBlock serverPingText) - { - while (!steamPingInfoReady) - { - yield return CoroutineStatus.Running; - } - - Steamworks.Data.NetPingLocation pingLocation = serverInfo.PingLocation.Value; - serverInfo.Ping = Steamworks.SteamNetworkingUtils.LocalPingLocation?.EstimatePingTo(pingLocation) ?? -1; - serverInfo.PingChecked = true; - serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); - serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; - - yield return CoroutineStatus.Success; - } - - private void ServerQueryFinished() - { - if (!serverList.Content.Children.Any()) - { - new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), - TextManager.Get("NoServers"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else if (serverList.Content.Children.All(c => !c.Visible)) - { - new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), - TextManager.Get("NoMatchingServers"), textAlignment: Alignment.Center) - { - CanBeFocused = false, - UserData = "noresults" - }; - } - waitingForRefresh = false; - } - - private void MasterServerCallBack(IRestResponse response) - { - masterServerResponse = response; - masterServerResponded = true; - } - - private bool JoinServer(string endpoint, string serverName) - { - if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) - { - ClientNameBox.Flash(); - ClientNameBox.Select(); - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - return false; - } - - MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; - GameSettings.SaveCurrentConfig(); - - CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName), "ConnectToServer"); - - return true; - } - - private IEnumerable ConnectToServer(string endpoint, string serverName) - { - string serverIP = null; - UInt64 serverSteamID = SteamManager.SteamIDStringToUInt64(endpoint); - if (serverSteamID == 0) { serverIP = endpoint; } - -#if !DEBUG - try - { -#endif - GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), serverIP, serverSteamID, serverName); -#if !DEBUG - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to start the client", e); - } -#endif - - yield return CoroutineStatus.Success; - } - - public void GetServerPing(ServerInfo serverInfo, GUITextBlock serverPingText) - { - if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } - - lock (activePings) - { - if (activePings.ContainsKey(serverInfo.IP)) { return; } - activePings.Add(serverInfo.IP, activePings.Any() ? activePings.Values.Max()+1 : 0); - } - - serverInfo.PingChecked = false; - serverInfo.Ping = -1; - - TaskPool.Add($"PingServerAsync ({serverInfo?.IP ?? "NULL"})", PingServerAsync(serverInfo.IP, 1000), - new Tuple(serverInfo, serverPingText), - (rtt, obj) => - { - var info = obj.Item1; - var text = obj.Item2; - rtt.TryGetResult(out info.Ping); info.PingChecked = true; - text.TextColor = GetPingTextColor(info.Ping); - text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; - lock (activePings) - { - activePings.Remove(info.IP); - } - }); - } - - private Color GetPingTextColor(int ping) - { - if (ping < 0) { return Color.DarkRed; } - return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); - } - - public async Task PingServerAsync(string ip, int timeOut) - { - await Task.Yield(); - bool shouldGo = false; - while (!shouldGo) - { - lock (activePings) - { - shouldGo = activePings.Count(kvp => kvp.Value < activePings[ip]) < 25; - } - await Task.Delay(25); - } - - if (string.IsNullOrWhiteSpace(ip)) - { - return -1; - } - - long rtt = -1; - IPAddress address = null; - IPAddress.TryParse(ip, out address); - if (address != null) - { - //don't attempt to ping if the address is IPv6 and it's not supported - if (address.AddressFamily != AddressFamily.InterNetworkV6 || Socket.OSSupportsIPv6) - { - Ping ping = new Ping(); - byte[] buffer = new byte[32]; - try - { - PingReply pingReply = ping.Send(address, timeOut, buffer, new PingOptions(128, true)); - - if (pingReply != null) - { - switch (pingReply.Status) - { - case IPStatus.Success: - rtt = pingReply.RoundtripTime; - break; - default: - rtt = -1; - break; - } - } - } - catch (Exception ex) - { - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ip, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); -#if DEBUG - DebugConsole.NewMessage("Failed to ping a server (" + ip + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); -#endif - } - } - } - - return (int)rtt; - } - - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) - { - graphics.Clear(Color.CornflowerBlue); - - GameMain.TitleScreen.DrawLoadingText = false; - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - - GUI.Draw(Cam, spriteBatch); - - spriteBatch.End(); - } - - public override void AddToGUIUpdateList() - { - menu.AddToGUIUpdateList(); - friendPopup?.AddToGUIUpdateList(); - friendsDropdown?.AddToGUIUpdateList(); - } - - public void StoreServerFilters() - { - foreach (KeyValuePair filterBox in filterTickBoxes) - { - ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); - } - foreach (KeyValuePair ternaryFilter in ternaryFilters) - { - ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); - } - } - - public void LoadServerFilters() - { - XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); - ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); - foreach (KeyValuePair filterBox in filterTickBoxes) - { - filterBox.Value.Selected = - ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); - } - foreach (KeyValuePair ternaryFilter in ternaryFilters) - { - TernaryOption ternaryOption = - ServerListFilters.Instance.GetAttributeEnum( - ternaryFilter.Key, - (TernaryOption)ternaryFilter.Value.SelectedData); - - var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); - ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); - } - } - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs new file mode 100644 index 000000000..f8cece982 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs @@ -0,0 +1,112 @@ +using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + public class PanelAnimator + { + private readonly GUIScissorComponent container; + + private readonly GUIFrame leftFrame; + private readonly GUIComponent middleFrame; + private readonly GUIFrame rightFrame; + + private readonly GUIButton leftButton; + private readonly GUIButton rightButton; + + private float leftAnimState = 1.0f; + private float rightAnimState = 0.0f; + + public bool LeftEnabled + { + get => leftButton.Enabled; + set => leftButton.Enabled = value; + } + public bool RightEnabled + { + get => rightButton.Enabled; + set => rightButton.Enabled = value; + } + + public bool LeftVisible = true; + public bool RightVisible = false; + + public PanelAnimator(RectTransform rectTransform, GUIFrame leftFrame, GUIComponent middleFrame, GUIFrame rightFrame) + { + container = new GUIScissorComponent(rectTransform); + + this.leftFrame = leftFrame; + this.middleFrame = middleFrame; + this.rightFrame = rightFrame; + + void own(GUIComponent component) + { + component.RectTransform.Parent = container.Content.RectTransform; + component.RectTransform.Anchor = Anchor.TopLeft; + component.RectTransform.Pivot = Pivot.TopLeft; + + component.GetAllChildren().ForEach(dd => dd.RefreshListBoxParent()); + } + + GUIButton makeButton(Action action) + => new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), container.Content.RectTransform) + { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, + style: "UIToggleButton") + { + OnClicked = (_, __) => + { + action(); + return false; + } + }; + + own(leftFrame); + this.leftButton = makeButton(() => LeftVisible = !LeftVisible); + + own(middleFrame); + + this.rightButton = makeButton(() => RightVisible = !RightVisible); + own(rightFrame); + } + + public void Update() + { + if (!LeftEnabled) { LeftVisible = false; } + if (!RightEnabled) { RightVisible = false; } + + static void updateState(ref float state, bool visible) + => state = MathHelper.Lerp(state, visible ? 0.0f : 1.0f, 0.5f); + updateState(ref leftAnimState, LeftVisible); + updateState(ref rightAnimState, RightVisible); + + static int width(GUIComponent c) + => c.RectTransform.NonScaledSize.X; + + int height = container.RectTransform.NonScaledSize.Y; + int buttonY = height/2 - leftButton.RectTransform.NonScaledSize.Y/2; + + leftFrame.RectTransform.AbsoluteOffset = new Point((int)(-width(leftFrame) * leftAnimState), 0); + leftButton.RectTransform.AbsoluteOffset = leftFrame.RectTransform.AbsoluteOffset + + new Point(width(leftFrame), buttonY); + leftButton.Children.ForEach(c => c.SpriteEffects = LeftVisible + ? SpriteEffects.FlipHorizontally + : SpriteEffects.None); + + rightFrame.RectTransform.AbsoluteOffset = new Point((int)(width(container) + width(rightFrame) * (rightAnimState-1f)), 0); + rightButton.RectTransform.AbsoluteOffset = rightFrame.RectTransform.AbsoluteOffset + + new Point(-width(rightButton), buttonY); + rightButton.Children.ForEach(c => c.SpriteEffects = RightVisible + ? SpriteEffects.None + : SpriteEffects.FlipHorizontally); + + middleFrame.RectTransform.AbsoluteOffset = new Point( + leftButton.RectTransform.AbsoluteOffset.X + width(leftButton), + 0); + middleFrame.RectTransform.NonScaledSize = new Point( + rightButton.RectTransform.AbsoluteOffset.X - middleFrame.RectTransform.AbsoluteOffset.X, + height); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs new file mode 100644 index 000000000..8e54407a4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -0,0 +1,1676 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class ServerListScreen : Screen + { + private enum MsgUserData + { + RefreshingServerList, + NoServers, + NoMatchingServers + } + + //how often the client is allowed to refresh servers + private static readonly TimeSpan AllowedRefreshInterval = TimeSpan.FromSeconds(3); + + private DateTime lastRefreshTime = DateTime.Now; + + private GUIFrame menu; + + private GUIListBox serverList; + private PanelAnimator panelAnimator; + private GUIFrame serverPreviewContainer; + private GUIListBox serverPreview; + + private GUIButton joinButton; + private Option selectedServer; + + private GUIButton scanServersButton; + + private enum TernaryOption + { + Any, + Enabled, + Disabled + } + + //friends list + public sealed class FriendInfo + { + public string Name; + + public readonly AccountId Id; + + public enum Status + { + Offline, + NotPlaying, + PlayingAnotherGame, + PlayingBarotrauma + } + + public readonly Status CurrentStatus; + + public string ServerName; + + public Option ConnectCommand; + public Option Avatar; + + public bool IsInServer + => CurrentStatus == Status.PlayingBarotrauma && ConnectCommand.IsSome(); + + public bool IsPlayingBarotrauma + => CurrentStatus == Status.PlayingBarotrauma; + + public bool PlayingAnotherGame + => CurrentStatus == Status.PlayingAnotherGame; + + public bool IsOnline + => CurrentStatus != Status.Offline; + + public LocalizedString StatusText + => CurrentStatus switch + { + Status.Offline => "", + _ when ConnectCommand.IsSome() + => TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName), + _ => TextManager.Get($"Friend{CurrentStatus}") + }; + + public FriendInfo(string name, AccountId id, Status status) + { + Name = name; + Id = id; + CurrentStatus = status; + ConnectCommand = Option.None(); + Avatar = Option.None(); + } + } + + private GUILayoutGroup friendsButtonHolder; + + private GUIButton friendsDropdownButton; + private GUIListBox friendsDropdown; + + private readonly FriendProvider friendProvider = new SteamFriendProvider(); + + private List friendsList; + private GUIFrame friendPopup; + private double friendsListUpdateTime; + + public enum TabEnum + { + All, + Favorites, + Recent + } + + public struct Tab + { + public readonly string Storage; + public readonly GUIButton Button; + + private readonly List servers; + public IReadOnlyList Servers => servers; + + public Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage) + { + Storage = storage; + servers = new List(); + Button = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabber.RectTransform), + TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton") + { + OnClicked = (_,__) => + { + serverListScreen.selectedTab = tabEnum; + return false; + } + }; + + Reload(); + } + + public void Reload() + { + if (Storage.IsNullOrEmpty()) { return; } + servers.Clear(); + XDocument doc = XMLExtensions.TryLoadXml(Storage, out _); + if (doc?.Root is null) { return; } + servers.AddRange(doc.Root.Elements().Select(ServerInfo.FromXElement).NotNone().Distinct()); + } + + public bool Contains(ServerInfo info) => servers.Contains(info); + public bool Remove(ServerInfo info) => servers.Remove(info); + public void AddOrUpdate(ServerInfo info) + { + servers.Remove(info); servers.Add(info); + } + + public void Clear() => servers.Clear(); + + public void Save() + { + XDocument doc = new XDocument(); + XElement rootElement = new XElement("servers"); + doc.Add(rootElement); + + foreach (ServerInfo info in servers) + { + rootElement.Add(info.ToXElement()); + } + + doc.SaveSafe(Storage); + } + } + + private readonly Dictionary tabs = new Dictionary(); + + private TabEnum _selectedTabBackingField; + private TabEnum selectedTab + { + get => _selectedTabBackingField; + set + { + _selectedTabBackingField = value; + tabs.ForEach(kvp => kvp.Value.Button.Selected = (value == kvp.Key)); + if (Screen.Selected == this) { RefreshServers(); } + } + } + + private readonly ServerProvider serverProvider + = new CompositeServerProvider(new SteamDedicatedServerProvider(), new SteamP2PServerProvider()); + + public GUITextBox ClientNameBox { get; private set; } + + enum ColumnLabel + { + ServerListCompatible, + ServerListHasPassword, + ServerListName, + ServerListRoundStarted, + ServerListPlayers, + ServerListPing + } + private struct Column + { + public float RelativeWidth; + public ColumnLabel Label; + + public static implicit operator Column((float W, ColumnLabel L) pair) => + new Column { RelativeWidth = pair.W, Label = pair.L }; + + public static Column[] Normalize(params Column[] columns) + { + var totalWidth = columns.Select(c => c.RelativeWidth).Aggregate((a, b) => a + b); + for (int i = 0; i < columns.Length; i++) + { + columns[i].RelativeWidth /= totalWidth; + } + return columns; + } + } + + private static readonly ImmutableDictionary columns = + Column.Normalize( + (0.1f, ColumnLabel.ServerListCompatible), + (0.1f, ColumnLabel.ServerListHasPassword), + (0.7f, ColumnLabel.ServerListName), + (0.12f, ColumnLabel.ServerListRoundStarted), + (0.08f, ColumnLabel.ServerListPlayers), + (0.08f, ColumnLabel.ServerListPing) + ).Select(c => (c.Label, c)).ToImmutableDictionary(); + + private GUILayoutGroup labelHolder; + private readonly List labelTexts = new List(); + + //filters + private GUITextBox searchBox; + private GUITickBox filterSameVersion; + private GUITickBox filterPassword; + private GUITickBox filterFull; + private GUITickBox filterEmpty; + private Dictionary ternaryFilters; + private Dictionary filterTickBoxes; + private Dictionary playStyleTickBoxes; + private Dictionary gameModeTickBoxes; + private GUITickBox filterOffensive; + + //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. + private TernaryOption filterFriendlyFireValue = TernaryOption.Any; + private TernaryOption filterKarmaValue = TernaryOption.Any; + private TernaryOption filterTraitorValue = TernaryOption.Any; + private TernaryOption filterVoipValue = TernaryOption.Any; + private TernaryOption filterModdedValue = TernaryOption.Any; + + private ColumnLabel sortedBy; + + private const float sidebarWidth = 0.2f; + public ServerListScreen() + { + selectedServer = Option.None(); + GameMain.Instance.ResolutionChanged += CreateUI; + CreateUI(); + } + + private string GetDefaultUserName() + { + return friendProvider.GetUserName(); + } + + private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) + { + var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) + { + Stretch = true + }; + + var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) + { + IsFixedSize = true, + }, null) + { + HoverColor = Color.Gray, + SelectedColor = Color.DarkGray, + CanBeFocused = false + }; + if (box.RectTransform.MinSize.Y > 0) + { + box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); + box.RectTransform.Resize(box.RectTransform.MinSize); + } + Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); + + var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) + { + UserData = TextManager.Get($"servertag.{tag}.label") + }; + GUIStyle.Apply(filterLabel, "GUITextBlock", null); + + var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); + dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); + dropDown.AddItem(TextManager.Get($"servertag.{tag}.true"), TernaryOption.Enabled, TextManager.Get( + $"servertagdescription.{tag}.true")); + dropDown.AddItem(TextManager.Get($"servertag.{tag}.false"), TernaryOption.Disabled, TextManager.Get( + $"servertagdescription.{tag}.false")); + dropDown.SelectItem(TernaryOption.Any); + dropDown.OnSelected = (_, data) => { + valueSetter((TernaryOption)data); + FilterServers(); + StoreServerFilters(); + return true; + }; + + ternaryFilters.Add(tag, dropDown); + } + + private void CreateUI() + { + menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + //------------------------------------------------------------------------------------- + //Top row + //------------------------------------------------------------------------------------- + + var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; + + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) + { + Padding = Vector4.Zero, + ForceUpperCase = ForceUpperCase.Yes, + AutoScaleHorizontal = true + }; + + var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; + + var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); + ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") + { + Text = MultiplayerPreferences.Instance.PlayerName, + MaxTextLength = Client.MaxNameLength, + OverflowClip = true + }; + + if (string.IsNullOrEmpty(ClientNameBox.Text)) + { + ClientNameBox.Text = GetDefaultUserName(); + } + ClientNameBox.OnTextChanged += (textbox, text) => + { + MultiplayerPreferences.Instance.PlayerName = text; + return true; + }; + + var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); + + tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, ""); + tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml"); + tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml"); + + var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") + { + IgnoreLayoutGroups = true + }; + + friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; + friendsList = new List(); + + //------------------------------------------------------------------------------------- + // Bottom row + //------------------------------------------------------------------------------------- + + var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y), + paddedFrame.RectTransform, Anchor.CenterRight)) + { + Stretch = true + }; + + var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + OutlineColor = Color.Black + }; + + GUILayoutGroup serverListContainer = null; + GUIFrame filtersHolder = null; + + // filters ------------------------------------------- + + filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + { + Color = new Color(12, 14, 15, 255) * 0.5f, + OutlineColor = Color.Black + }; + + float elementHeight = 0.05f; + var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) + { + Padding = Vector4.Zero, + AutoScaleHorizontal = true, + CanBeFocused = false + }; + + var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true }; + + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); + searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; + + var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft)) + { + ScrollBarVisible = true, + Spacing = (int)(5 * GUI.Scale) + }; + + ternaryFilters = new Dictionary(); + filterTickBoxes = new Dictionary(); + + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) + { + text ??= TextManager.Get(key); + var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + { + UserData = text, + Selected = defaultState, + ToolTip = addTooltip ? text : null, + OnSelected = (tickBox) => + { + FilterServers(); + StoreServerFilters(); + return true; + } + }; + filterTickBoxes.Add(key, tickBox); + return tickBox; + } + + filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); + filterPassword = addTickBox("FilterPassword".ToIdentifier()); + filterFull = addTickBox("FilterFullServers".ToIdentifier()); + filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); + filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + + // Filter Tags + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); + + // Play Style Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + playStyleTickBoxes = new Dictionary(); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); + selectionTick.UserData = playStyle; + playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); + } + + // Game mode Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + + gameModeTickBoxes = new Dictionary(); + foreach (GameModePreset mode in GameModePreset.List) + { + if (mode.IsSinglePlayer) { continue; } + + var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); + selectionTick.UserData = mode.Identifier; + gameModeTickBoxes.Add(mode.Identifier, selectionTick); + } + + filters.Content.RectTransform.SizeChanged += () => + { + filters.Content.RectTransform.RecalculateChildren(true, true); + filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); + gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + GUITextBlock.AutoScaleAndNormalize( + filterTickBoxes.Values.Select(tb => tb.TextBlock) + .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), + defaultScale: 1.0f); + if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) + { + filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); + filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f))); + } + }; + + // server list --------------------------------------------------------------------- + + serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; + + labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, + isHorizontal: true, childAnchor: Anchor.BottomLeft) + { + Stretch = false + }; + + foreach (var column in columns.Values) + { + var label = TextManager.Get(column.Label.ToString()); + var btn = new GUIButton(new RectTransform(new Vector2(column.RelativeWidth, 1.0f), labelHolder.RectTransform), + text: label, textAlignment: Alignment.Center, style: "GUIButtonSmall") + { + ToolTip = label, + ForceUpperCase = ForceUpperCase.Yes, + UserData = column.Label, + OnClicked = SortList + }; + btn.Color *= 0.5f; + labelTexts.Add(btn.TextBlock); + + GUIImage arrowImg(object userData, SpriteEffects sprEffects) + => new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) + { + CanBeFocused = false, + UserData = userData, + SpriteEffects = sprEffects, + Visible = false + }; + + arrowImg("arrowup", SpriteEffects.None); + arrowImg("arrowdown", SpriteEffects.FlipVertically); + } + + serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) + { + PlaySoundOnSelect = true, + ScrollBarVisible = true, + OnSelected = (btn, obj) => + { + if (!(obj is ServerInfo serverInfo)) { return false; } + + joinButton.Enabled = true; + selectedServer = Option.Some(serverInfo); + if (!serverPreviewContainer.Visible) + { + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); + serverPreviewContainer.Visible = true; + serverPreviewContainer.IgnoreLayoutGroups = false; + } + serverInfo.CreatePreviewWindow(serverPreview.Content); + serverPreview.ForceLayoutRecalculation(); + panelAnimator.RightEnabled = true; + panelAnimator.RightVisible = true; + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + return true; + } + }; + + //server preview panel -------------------------------------------------- + serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + { + Color = new Color(12, 14, 15, 255) * 0.5f, + OutlineColor = Color.Black, + IgnoreLayoutGroups = true + }; + serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) + { + Padding = Vector4.One * 10 * GUI.Scale, + HoverCursor = CursorState.Default, + OnSelected = (component, o) => false + }; + + panelAnimator = new PanelAnimator(new RectTransform(Vector2.One, serverListHolder.RectTransform), + filtersHolder, + serverListContainer, + serverPreviewContainer); + panelAnimator.RightEnabled = false; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Back")) + { + OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu + }; + + scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("ServerListRefresh")) + { + OnClicked = (btn, userdata) => { RefreshServers(); return true; } + }; + + var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("serverlistdirectjoin")) + { + OnClicked = (btn, userdata) => + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + return false; + } + ShowDirectJoinPrompt(); + return true; + } + }; + + joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("ServerListJoin")) + { + OnClicked = (btn, userdata) => + { + if (selectedServer.TryUnwrap(out var serverInfo)) + { + JoinServer(serverInfo.Endpoint, serverInfo.ServerName); + } + return true; + }, + Enabled = false + }; + + buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f)); + + //-------------------------------------------------------- + + bottomRow.Recalculate(); + serverListHolder.Recalculate(); + serverListContainer.Recalculate(); + labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); + labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); + labelHolder.Recalculate(); + + serverList.Content.RectTransform.SizeChanged += () => + { + labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); + labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); + labelHolder.Recalculate(); + foreach (GUITextBlock labelText in labelTexts) + { + labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); + } + }; + + button.SelectedColor = button.Color; + + selectedTab = TabEnum.All; + } + + public void UpdateOrAddServerInfo(ServerInfo serverInfo) + { + GUIComponent existingElement = serverList.Content.FindChild(d => + d.UserData is ServerInfo existingServerInfo && + existingServerInfo.Endpoint == serverInfo.Endpoint); + if (existingElement == null) + { + AddToServerList(serverInfo); + } + else + { + existingElement.UserData = serverInfo; + } + } + + public void AddToRecentServers(ServerInfo info) + { + if (info.Endpoint.Address.IsLocalHost) { return; } + tabs[TabEnum.Recent].AddOrUpdate(info); + tabs[TabEnum.Recent].Save(); + } + + public bool IsFavorite(ServerInfo info) + => tabs[TabEnum.Favorites].Contains(info); + + public void AddToFavoriteServers(ServerInfo info) + { + tabs[TabEnum.Favorites].AddOrUpdate(info); + tabs[TabEnum.Favorites].Save(); + } + + public void RemoveFromFavoriteServers(ServerInfo info) + { + tabs[TabEnum.Favorites].Remove(info); + tabs[TabEnum.Favorites].Save(); + } + + private bool SortList(GUIButton button, object obj) + { + if (!(obj is ColumnLabel sortBy)) { return false; } + SortList(sortBy, toggle: true); + return true; + } + + private void SortList(ColumnLabel sortBy, bool toggle) + { + if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + + sortedBy = sortBy; + + var arrowUp = button.GetChildByUserData("arrowup"); + var arrowDown = button.GetChildByUserData("arrowdown"); + + //disable arrow buttons in other labels + foreach (var child in button.Parent.Children) + { + if (child != button) + { + child.GetChildByUserData("arrowup").Visible = false; + child.GetChildByUserData("arrowdown").Visible = false; + } + } + + bool ascending = arrowUp.Visible; + if (toggle) + { + ascending = !ascending; + } + + arrowUp.Visible = ascending; + arrowDown.Visible = !ascending; + serverList.Content.RectTransform.SortChildren((c1, c2) => + { + if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } + if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } + + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + }); + } + + public override void Select() + { + base.Select(); + + Steamworks.SteamMatchmaking.ResetActions(); + + selectedTab = TabEnum.All; + GameMain.ServerListScreen.LoadServerFilters(); + if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) + { + var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("FilterOffensiveServersPrompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => + { + filterOffensive.Selected = true; + filterOffensivePrompt.Close(); + return true; + }; + filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; + + var config = GameSettings.CurrentConfig; + config.ShowOffensiveServerPrompt = false; + GameSettings.SetCurrentConfig(config); + } + + if (GameMain.Client != null) + { + GameMain.Client.Quit(); + GameMain.Client = null; + } + + RefreshServers(); + } + + public override void Deselect() + { + base.Deselect(); + GameSettings.SaveCurrentConfig(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + + UpdateFriendsList(); + panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; + + if (PlayerInput.PrimaryMouseButtonClicked()) + { + friendPopup = null; + if (friendsDropdown != null && friendsDropdownButton != null && + !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && + !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) + { + friendsDropdown.Visible = false; + } + } + } + + private void FilterServers() + { + RemoveMsgFromServerList(MsgUserData.NoMatchingServers); + foreach (GUIComponent child in serverList.Content.Children) + { + if (!(child.UserData is ServerInfo serverInfo)) { continue; } + child.Visible = ShouldShowServer(serverInfo); + } + + if (serverList.Content.Children.All(c => !c.Visible)) + { + PutMsgInServerList(MsgUserData.NoMatchingServers); + } + serverList.UpdateScrollBarSize(); + } + + private bool ShouldShowServer(ServerInfo serverInfo) + { +#if !DEBUG + //never show newer versions + //(ignore revision number, it doesn't affect compatibility) + if (ToolBox.VersionNewerIgnoreRevision(GameMain.Version, serverInfo.GameVersion)) + { + return false; + } +#endif + + if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } + + if (filterSameVersion.Selected) + { + if (!NetworkMember.IsCompatible(serverInfo.GameVersion, GameMain.Version)) { return false; } + } + if (filterPassword.Selected) + { + if (serverInfo.HasPassword) { return false; } + } + if (filterFull.Selected) + { + if (serverInfo.PlayerCount >= serverInfo.MaxPlayers) { return false; } + } + if (filterEmpty.Selected) + { + if (serverInfo.PlayerCount <= 0) { return false; } + } + if (filterOffensive.Selected) + { + if (ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) { return false; } + } + + if (filterKarmaValue != TernaryOption.Any) + { + if (serverInfo.KarmaEnabled != (filterKarmaValue == TernaryOption.Enabled)) { return false; } + } + if (filterFriendlyFireValue != TernaryOption.Any) + { + if (serverInfo.FriendlyFireEnabled != (filterFriendlyFireValue == TernaryOption.Enabled)) { return false; } + } + if (filterTraitorValue != TernaryOption.Any) + { + if ((serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) != (filterTraitorValue == TernaryOption.Enabled)) + { + return false; + } + } + if (filterVoipValue != TernaryOption.Any) + { + if (serverInfo.VoipEnabled != (filterVoipValue == TernaryOption.Enabled)) { return false; } + } + if (filterModdedValue != TernaryOption.Any) + { + if (serverInfo.IsModded != (filterModdedValue == TernaryOption.Enabled)) { return false; } + } + + foreach (GUITickBox tickBox in playStyleTickBoxes.Values) + { + var playStyle = (PlayStyle)tickBox.UserData; + if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) + { + return false; + } + } + + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) + { + var gameMode = (Identifier)tickBox.UserData; + if (!tickBox.Selected && !serverInfo.GameMode.IsEmpty && serverInfo.GameMode == gameMode) + { + return false; + } + } + + return true; + } + + private void ShowDirectJoinPrompt() + { + var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", + new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); + msgBox.Content.ChildAnchor = Anchor.TopCenter; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter) + { + IgnoreLayoutGroups = false, + Stretch = true, + RelativeSpacing = 0.05f + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); + var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); + + content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); + content.RectTransform.IsFixedSize = true; + msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f)); + + var okButton = msgBox.Buttons[0]; + okButton.Enabled = false; + okButton.OnClicked = (btn, userdata) => + { + if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } + JoinServer(endpoint, ""); + msgBox.Close(); + return false; + }; + + var favoriteButton = msgBox.Buttons[1]; + favoriteButton.Enabled = false; + favoriteButton.OnClicked = (button, userdata) => + { + if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } + + var serverInfo = new ServerInfo(endpoint) + { + ServerName = "Server", + GameVersion = GameMain.Version + }; + + var serverFrame = serverList.Content.FindChild(d => + d.UserData is ServerInfo info + && info.Equals(serverInfo)); + + if (serverFrame != null) + { + serverInfo = (ServerInfo)serverFrame.UserData; + } + else + { + AddToServerList(serverInfo); + } + + AddToFavoriteServers(serverInfo); + + selectedTab = TabEnum.Favorites; + FilterServers(); + + #warning Interface with server providers to get up-to-date info on the given server + + msgBox.Close(); + return false; + }; + + var cancelButton = msgBox.Buttons[2]; + cancelButton.OnClicked = msgBox.Close; + + endpointBox.OnTextChanged += (textBox, text) => + { + okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); + return true; + }; + } + + private bool JoinFriend(GUIButton button, object userdata) + { + if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } + + GameMain.Instance.ConnectCommand = info.ConnectCommand; + return false; + } + + private bool OpenFriendPopup(GUIButton button, object userdata) + { + if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } + + if (info.IsInServer + && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } + && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + { + const int framePadding = 5; + + friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); + + var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), nameAndEndpoint.ServerName ?? "[Unnamed]"); + serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); + + var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) + { + UserData = info + }; + joinButton.OnClicked = JoinFriend; + joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); + + Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); + int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; + int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); + + // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. + Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); + + // Add padding to the server name to match the padding on the button text. + serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); + + // Get the dimensions of the text we want to show, plus the extra padding we added. + Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); + + // Now determine how large the parent frame has to be to exactly fit our two controls. + Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); + + var popupPos = PlayerInput.MousePosition.ToPoint(); + if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) + { + // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. + popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; + } + + // Apply the size and position changes. + friendPopup.RectTransform.NonScaledSize = frameDims; + friendPopup.RectTransform.RelativeOffset = Vector2.Zero; + friendPopup.RectTransform.AbsoluteOffset = popupPos; + + joinButton.RectTransform.NonScaledSize = finalButtonSize; + + friendPopup.RectTransform.RecalculateChildren(true); + friendPopup.RectTransform.SetPosition(Anchor.TopLeft); + } + + return false; + } + + public enum AvatarSize + { + Small, + Medium, + Large + } + + private void UpdateFriendsList() + { + if (friendsListUpdateTime > Timing.TotalTime) { return; } + friendsListUpdateTime = Timing.TotalTime + 5.0; + + float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; + + friendsDropdown ??= new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) + { + OutlineColor = Color.Black, + Visible = false + }; + friendsDropdown.ClearChildren(); + + var avatarSize = friendsButtonHolder.RectTransform.Rect.Height switch + { + var h when h <= 24 => AvatarSize.Small, + var h when h <= 48 => AvatarSize.Medium, + _ => AvatarSize.Large + }; + + FriendInfo[] friends = friendProvider.RetrieveFriends(); + + foreach (var friend in friends) + { + int existingIndex = friendsList.FindIndex(f => f.Id == friend.Id); + if (existingIndex >= 0) + { + friend.Avatar = friend.Avatar.Fallback(friendsList[existingIndex].Avatar); + } + + if (friend.Avatar.IsNone()) + { + friendProvider.RetrieveAvatar(friend, avatarSize); + } + } + + friendsList.Clear(); friendsList.AddRange(friends.OrderByDescending(f => f.CurrentStatus)); + + friendsButtonHolder.ClearChildren(); + + if (friendsList.Count > 0) + { + friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") + { + OnClicked = (button, udt) => + { + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); + friendsDropdown.RectTransform.RecalculateChildren(true); + friendsDropdown.Visible = !friendsDropdown.Visible; + return false; + } + }; + } + else + { + friendsDropdownButton = null; + friendsDropdown.Visible = false; + } + + for (int i = 0; i < friendsList.Count; i++) + { + var friend = friendsList[i]; + + if (i < 5) + { + string style = friend.IsPlayingBarotrauma + ? "GUIButtonFriendPlaying" + : "GUIButtonFriendNotPlaying"; + + var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) + { + UserData = friend, + OnClicked = OpenFriendPopup + }; + guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; + + if (friend.Avatar.TryUnwrap(out Sprite sprite)) + { + new GUICustomComponent(new RectTransform(Vector2.One, guiButton.RectTransform, Anchor.Center), + onDraw: (sb, component) => + { + var destinationRect = component.Rect; + destinationRect.Inflate(-GUI.IntScale(4), -GUI.IntScale(4)); + sb.Draw(sprite.Texture, destinationRect, Color.White); + + if (!GUI.IsMouseOn(guiButton)) + { + return; + } + + sb.End(); + sb.Begin( + SpriteSortMode.Deferred, + blendState: BlendState.Additive, + samplerState: GUI.SamplerState, + rasterizerState: GameMain.ScissorTestEnable); + sb.Draw(sprite.Texture, destinationRect, Color.White * 0.5f); + sb.End(); + sb.Begin( + SpriteSortMode.Deferred, + samplerState: GUI.SamplerState, + rasterizerState: GameMain.ScissorTestEnable); + }) { CanBeFocused = false }; + } + } + + var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); + if (friend.Avatar.TryUnwrap(out var avatar)) + { + GUIImage guiImage = + new GUIImage( + new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, + scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) }, + avatar, null, true); + } + + var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) + { + Font = GUIStyle.SmallFont + }; + if (friend.IsPlayingBarotrauma) { textBlock.TextColor = GUIStyle.Green; } + if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } + + if (friend.IsInServer) + { + var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") + { + UserData = friend, + OnClicked = JoinFriend + }; + } + } + + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); + friendsDropdown.RectTransform.RecalculateChildren(true); + + friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; + } + + private void RemoveMsgFromServerList() + { + serverList.Content.Children + .Where(c => c.UserData is MsgUserData) + .ForEachMod(serverList.Content.RemoveChild); + } + + private void RemoveMsgFromServerList(MsgUserData userData) + { + serverList.Content.RemoveChild(serverList.Content.FindChild(userData)); + } + + private void PutMsgInServerList(MsgUserData userData) + { + RemoveMsgFromServerList(); + new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), + TextManager.Get(userData.ToString()), textAlignment: Alignment.Center) + { + CanBeFocused = false, + UserData = userData + }; + } + + private void RefreshServers() + { + lastRefreshTime = DateTime.Now; + serverProvider.Cancel(); + currentServerDataRecvCallbackObj = null; + + PingUtils.QueryPingData(); + + tabs[TabEnum.All].Clear(); + serverList.ClearChildren(); + serverPreview.Content.ClearChildren(); + panelAnimator.RightEnabled = false; + joinButton.Enabled = false; + selectedServer = null; + + if (selectedTab == TabEnum.All) + { + PutMsgInServerList(MsgUserData.RefreshingServerList); + } + else + { + var servers = tabs[selectedTab].Servers.ToArray(); + foreach (var server in servers) + { + server.Ping = Option.None(); + AddToServerList(server, skipPing: true); + } + + if (!servers.Any()) + { + PutMsgInServerList(MsgUserData.NoServers); + return; + } + } + + var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks(); + serverProvider.RetrieveServers(onServerDataReceived, onQueryCompleted); + } + + private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo) + { + bool matches(GUIComponent c) + => c.UserData is ServerInfo info + && info.Equals(serverInfo); + +#if DEBUG + if (serverList.Content.Children.Count(matches) > 1) + { + DebugConsole.ThrowError($"There are several entries in the server list for endpoint {serverInfo.Endpoint}"); + } +#endif + + return serverList.Content.FindChild(matches); + } + + private object currentServerDataRecvCallbackObj = null; + private (Action OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks() + { + var uniqueObject = new object(); + currentServerDataRecvCallbackObj = uniqueObject; + + bool shouldRunCallback() + { + // If currentServerDataRecvCallbackObj != uniqueObject, then one of the following happened: + // - The query this call is associated to was meant to be over + // - Another query was started before the one associated to this call was finished + // In either case, do not add the received info to the server list. + return ReferenceEquals(currentServerDataRecvCallbackObj, uniqueObject); + } + + return ( + serverInfo => + { + if (!shouldRunCallback()) { return; } + + if (selectedTab == TabEnum.All) + { + AddToServerList(serverInfo); + } + else + { + if (FindFrameMatchingServerInfo(serverInfo) == null) { return; } + UpdateServerInfoUI(serverInfo); + PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); + } + }, + () => + { + if (shouldRunCallback()) { ServerQueryFinished(); } + } + ); + } + + private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) + { + RemoveMsgFromServerList(MsgUserData.RefreshingServerList); + RemoveMsgFromServerList(MsgUserData.NoServers); + var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, + style: "ListBoxElement") + { + UserData = serverInfo + }; + new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = false + }; + UpdateServerInfoUI(serverInfo); + if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } + + SortList(sortedBy, toggle: false); + FilterServers(); + } + + private void UpdateServerInfoUI(ServerInfo serverInfo) + { + var serverFrame = FindFrameMatchingServerInfo(serverInfo); + if (serverFrame == null) { return; } + + serverFrame.UserData = serverInfo; + + serverFrame.ToolTip = ""; + var serverContent = serverFrame.Children.First() as GUILayoutGroup; + serverContent.ClearChildren(); + + Dictionary sections = new Dictionary(); + foreach (ColumnLabel label in Enum.GetValues(typeof(ColumnLabel))) + { + sections[label] = + new GUIFrame( + new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform), + style: null); + } + + void errorTooltip(RichString toolTip) + { + sections.Values.ForEach(c => + { + c.CanBeFocused = false; + c.Children.First().CanBeFocused = false; + }); + serverFrame.ToolTip = toolTip; + } + + RectTransform columnRT(ColumnLabel label, float scale = 0.95f) + => new RectTransform(Vector2.One * scale, sections[label].RectTransform, Anchor.Center); + + void sectionTooltip(ColumnLabel label, RichString toolTip) + { + var section = sections[label]; + section.CanBeFocused = true; + section.ToolTip = toolTip; + } + + var compatibleBox = new GUITickBox(columnRT(ColumnLabel.ServerListCompatible), label: "") + { + CanBeFocused = false, + Selected = + NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion), + UserData = "compatible" + }; + + var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox") + { + Selected = serverInfo.HasPassword, + UserData = "password", + CanBeFocused = false + }; + sectionTooltip(ColumnLabel.ServerListHasPassword, + TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword")); + + var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName), +#if DEBUG + $"[{serverInfo.Endpoint.GetType().Name}] " + +#endif + serverInfo.ServerName, + style: "GUIServerListTextBox") { CanBeFocused = false }; + + if (serverInfo.IsModded) + { + serverName.TextColor = GUIStyle.ModdedServerColor; + } + + new GUITickBox(columnRT(ColumnLabel.ServerListRoundStarted), label: "") + { + Selected = serverInfo.GameStarted, + CanBeFocused = false + }; + sectionTooltip(ColumnLabel.ServerListRoundStarted, + TextManager.Get(serverInfo.GameStarted ? "ServerListRoundStarted" : "ServerListRoundNotStarted")); + + var serverPlayers = new GUITextBlock(columnRT(ColumnLabel.ServerListPlayers), + $"{serverInfo.PlayerCount}/{serverInfo.MaxPlayers}", style: "GUIServerListTextBox", textAlignment: Alignment.Right) + { + ToolTip = TextManager.Get("ServerListPlayers") + }; + + var serverPingText = new GUITextBlock(columnRT(ColumnLabel.ServerListPing), "?", + style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) + { + ToolTip = TextManager.Get("ServerListPing") + }; + + if (serverInfo.Ping.TryUnwrap(out var ping)) + { + serverPingText.Text = ping.ToString(); + serverPingText.TextColor = GetPingTextColor(ping); + } + else + { + serverPingText.Text = "?"; + serverPingText.TextColor = Color.DarkRed; + } + + if (!serverInfo.Checked) + { + errorTooltip(TextManager.Get("ServerOffline")); + serverName.TextColor *= 0.8f; + serverPlayers.TextColor *= 0.8f; + } + else if (!serverInfo.ContentPackages.Any()) + { + compatibleBox.Selected = false; + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), + " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) + { + ToolTip = TextManager.Get("ServerListUnknownContentPackage") + }; + } + else if (!compatibleBox.Selected) + { + LocalizedString toolTip = ""; + if (serverInfo.GameVersion != GameMain.Version) + { + toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); + } + + int maxIncompatibleToList = 10; + List incompatibleModNames = new List(); + foreach (var contentPackage in serverInfo.ContentPackages) + { + bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(cp => cp.Hash.StringRepresentation == contentPackage.Hash); + if (listAsIncompatible) + { + incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", + ("[name]", contentPackage.Name), + ("[hash]", Md5Hash.GetShortHash(contentPackage.Hash)))); + } + } + if (incompatibleModNames.Any()) + { + toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); + if (incompatibleModNames.Count > maxIncompatibleToList) + { + toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); + } + } + errorTooltip(toolTip); + + serverName.TextColor *= 0.5f; + serverPlayers.TextColor *= 0.5f; + } + else + { + LocalizedString toolTip = ""; + foreach (var contentPackage in serverInfo.ContentPackages) + { + if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) + { + if (toolTip != "") { toolTip += "\n"; } + toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", contentPackage.Name); + break; + } + } + errorTooltip(toolTip); + } + + foreach (var section in sections.Values) + { + var child = section.Children.First(); + child.RectTransform.ScaleBasis + = child is GUITextBlock ? ScaleBasis.Normal : ScaleBasis.BothHeight; + } + + // The next twenty-something lines are an optimization. + // The issue is that the serverlist has a ton of text elements, + // and resizing all of them is extremely expensive. However, since + // you don't see most of them most of the time, it makes sense to + // just resize them lazily based on when you actually can see them. + // That would entail a UI refactor of some kind, and I don't want to + // do that just yet, so here's a hack instead! + bool isDirty = true; + void markAsDirty() => isDirty = true; + serverContent.GetAllChildren().ForEach(c => + { + c.RectTransform.ResetSizeChanged(); + c.RectTransform.SizeChanged += markAsDirty; + }); + new GUICustomComponent(new RectTransform(Vector2.Zero, serverContent.RectTransform), onUpdate: (_, __) => + { + if (serverFrame.MouseRect.Height <= 0 || !isDirty) { return; } + serverContent.GetAllChildren().ForEach(c => + { + switch (c) + { + case GUITextBlock textBlock: + textBlock.SetTextPos(); + break; + case GUITickBox tickBox: + tickBox.ResizeBox(); + break; + } + }); + serverName.Text = ToolBox.LimitString(serverInfo.ServerName, serverName.Font, serverName.Rect.Width); + isDirty = false; + }); + // Hacky optimization ends here + + serverContent.Recalculate(); + + if (tabs[TabEnum.Favorites].Contains(serverInfo)) + { + AddToFavoriteServers(serverInfo); + } + + SortList(sortedBy, toggle: false); + FilterServers(); + } + + private void ServerQueryFinished() + { + currentServerDataRecvCallbackObj = null; + if (!serverList.Content.Children.Any(c => c.UserData is ServerInfo)) + { + PutMsgInServerList(MsgUserData.NoServers); + } + else if (serverList.Content.Children.All(c => !c.Visible)) + { + PutMsgInServerList(MsgUserData.NoMatchingServers); + } + } + + public void JoinServer(Endpoint endpoint, string serverName) + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + return; + } + + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; + GameSettings.SaveCurrentConfig(); + +#if !DEBUG + try + { +#endif + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(GetDefaultUserName()), endpoint, serverName, Option.None()); +#if !DEBUG + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to start the client", e); + } +#endif + } + + private Color GetPingTextColor(int ping) + { + if (ping < 0) { return Color.DarkRed; } + return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + graphics.Clear(Color.CornflowerBlue); + + GameMain.TitleScreen.DrawLoadingText = false; + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); + + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + + GUI.Draw(Cam, spriteBatch); + + spriteBatch.End(); + } + + public override void AddToGUIUpdateList() + { + menu.AddToGUIUpdateList(); + friendPopup?.AddToGUIUpdateList(); + friendsDropdown?.AddToGUIUpdateList(); + } + + public void StoreServerFilters() + { + foreach (KeyValuePair filterBox in filterTickBoxes) + { + ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); + } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); + } + } + + public void LoadServerFilters() + { + XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + foreach (KeyValuePair filterBox in filterTickBoxes) + { + filterBox.Value.Selected = + ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); + } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + TernaryOption ternaryOption = + ServerListFilters.Instance.GetAttributeEnum( + ternaryFilter.Key, + (TernaryOption)ternaryFilter.Value.SelectedData); + + var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); + ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); + } + } + + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 0885055c7..848eba458 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -149,7 +149,7 @@ namespace Barotrauma private GUIDropDown linkedSubBox; private static GUIComponent autoSaveLabel; - private static int maxAutoSaves => GameSettings.CurrentConfig.MaxAutoSaves; + private static int MaxAutoSaves => GameSettings.CurrentConfig.MaxAutoSaves; public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); @@ -228,6 +228,8 @@ namespace Barotrauma private static bool isAutoSaving; + private KeyOrMouse toggleEntityListBind; + public override Camera Cam => cam; public static XDocument AutoSaveInfo; @@ -813,8 +815,13 @@ namespace Barotrauma var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => { - itemCount.TextColor = Item.ItemList.Count > MaxItems ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Item.ItemList.Count / (float)MaxItems); - return Item.ItemList.Count.ToString(); + int count = Item.ItemList.Count; + if (dummyCharacter?.Inventory != null) + { + count -= dummyCharacter.Inventory.AllItems.Count(); + } + itemCount.TextColor = count > MaxItems ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxItems); + return count.ToString(); }; var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), @@ -921,7 +928,6 @@ 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 = RichString.Rich($"{TextManager.Get("EntityMenuToggleTooltip")}\n‖color:125,125,125‖{GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].Name}‖color:end‖"), OnClicked = (btn, userdata) => { entityMenuOpen = !entityMenuOpen; @@ -1503,7 +1509,7 @@ namespace Barotrauma /// private static IEnumerable AutoSaveCoroutine() { - DateTime target = DateTime.Now.AddMinutes(GameSettings.CurrentConfig.AutoSaveIntervalSeconds); + DateTime target = DateTime.Now.AddSeconds(GameSettings.CurrentConfig.AutoSaveIntervalSeconds); DateTime tempTarget = DateTime.Now; bool wasPaused = false; @@ -1547,6 +1553,8 @@ namespace Barotrauma GUI.ForceMouseOn(null); + if (ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); } + MapEntityPrefab.Selected = null; saveFrame = null; @@ -1555,7 +1563,9 @@ namespace Barotrauma MapEntity.DeselectAll(); ClearUndoBuffer(); +#if !DEBUG DebugConsole.DeactivateCheats(); +#endif SetMode(Mode.Default); @@ -1644,7 +1654,7 @@ namespace Barotrauma if (AutoSaveInfo?.Root == null || MainSub?.Info == null) { return; } int saveCount = AutoSaveInfo.Root.Elements().Count(); - while (AutoSaveInfo.Root.Elements().Count() > maxAutoSaves) + while (AutoSaveInfo.Root.Elements().Count() > MaxAutoSaves) { XElement min = AutoSaveInfo.Root.Elements().OrderBy(element => element.GetAttributeUInt64("time", 0)).FirstOrDefault(); #warning TODO: revise @@ -1795,25 +1805,14 @@ namespace Barotrauma { Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); - void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) + static string getExistingFilePath(ContentPackage package, string fileName) { - filePath = filePath.CleanUpPath(); - packagePath = packagePath.CleanUpPath(); - string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); - if (filePath.StartsWith(packageDir)) + if (Submarine.MainSub?.Info == null) { return null; } + if (package.Files.Any(f => f.Path == MainSub.Info.FilePath && Path.GetFileName(f.Path.Value) == fileName)) { - filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + return MainSub.Info.FilePath; } - if (!modProject.Files.Any(f => f.Type == subFileType && - f.Path == filePath)) - { - var newFile = ModProject.File.FromPath(filePath, subFileType); - modProject.AddFile(newFile); - } - - using var _ = Validation.SkipInDebugBuilds(); - modProject.DiscardHashAndInstallTime(); - modProject.Save(packagePath); + return null; } if (!GameMain.DebugDraw) @@ -1837,7 +1836,7 @@ namespace Barotrauma return false; } - foreach (var illegalChar in Path.GetInvalidFileNameChars()) + foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (!name.Contains(illegalChar)) { continue; } GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); @@ -1859,101 +1858,139 @@ namespace Barotrauma #if !DEBUG throw new InvalidOperationException("Cannot save to Vanilla package"); #endif - savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch - { - SubmarineType.Player => "Content/Submarines/{0}", - SubmarineType.Outpost => "Content/Map/Outposts/{0}", - SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... - SubmarineType.Wreck => "Content/Map/Wrecks/{0}", - SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", - SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", - SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", - _ => throw new InvalidOperationException() - }, savePath); + savePath = + getExistingFilePath(packageToSaveTo, savePath) ?? + string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch + { + SubmarineType.Player => "Content/Submarines/{0}", + SubmarineType.Outpost => "Content/Map/Outposts/{0}", + SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... + SubmarineType.Wreck => "Content/Map/Wrecks/{0}", + SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", + SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", + SubmarineType.OutpostModule => MainSub.Info.FilePath.Contains("RuinModules") ? "Content/Map/RuinModules/{0}" : "Content/Map/Outposts/{0}", + _ => throw new InvalidOperationException() + }, savePath); modProject.ModVersion = ""; } else { - savePath = Path.Combine(packageToSaveTo.Dir, savePath); + string existingFilePath = getExistingFilePath(packageToSaveTo, savePath); + //if we're trying to save a sub that's already included in the package with the same name as before, save directly in the same path + if (existingFilePath != null) + { + savePath = existingFilePath; + } + //otherwise make sure we're not trying to overwrite another sub in the same package + else + { + savePath = Path.Combine(packageToSaveTo.Dir, savePath); + if (File.Exists(savePath)) + { + var verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + verification.Buttons[0].OnClicked = (_, _) => + { + addSubAndSave(modProject, savePath, fileListPath); + verification.Close(); + return true; + }; + verification.Buttons[1].OnClicked = verification.Close; + return false; + } + } } - addSubAndSaveModProject(modProject, savePath, fileListPath); - } - else if (MainSub?.Info?.FilePath != null - && MainSub.Info.Name != null - && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) - && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - prevSavePath = MainSub.Info.FilePath.CleanUpPath(); - ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); - if (contentPackage == null) - { - throw new InvalidOperationException($"Tried to overwrite a submarine ({name}) that's not in a local package!"); - } - ModProject modProject = new ModProject(contentPackage); - packageToSaveTo = contentPackage; - savePath = prevSavePath; - addSubAndSaveModProject(modProject, savePath, contentPackage.Path); + addSubAndSave(modProject, savePath, fileListPath); } else { savePath = Path.Combine(newLocalModDir, savePath); - ModProject modProject = new ModProject { Name = name }; - addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); - } - savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); - - if (MainSub != null) - { - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) + if (File.Exists(savePath)) { - bool savePreviewImage = true; - using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); - try - { - previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); - savePreviewImage = false; - } - MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); + new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariable("subeditor.packagealreadyexists", "[name]", name)); + return false; } else { - MainSub.TrySaveAs(savePath); + ModProject modProject = new ModProject { Name = name }; + addSubAndSave(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + } - MainSub.CheckForErrors(); - - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); - - if (savePath.StartsWith(newLocalModDir)) + void addSubAndSave(ModProject modProject, string filePath, string packagePath) + { + filePath = filePath.CleanUpPath(); + packagePath = packagePath.CleanUpPath(); + string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + if (filePath.StartsWith(packageDir)) { - ContentPackageManager.LocalPackages.Refresh(); - var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); - if (newPackage is RegularPackage regular) + filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + } + if (!modProject.Files.Any(f => f.Type == subFileType && + f.Path == filePath)) + { + var newFile = ModProject.File.FromPath(filePath, subFileType); + modProject.AddFile(newFile); + } + + using var _ = Validation.SkipInDebugBuilds(); + modProject.DiscardHashAndInstallTime(); + modProject.Save(packagePath); + + savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); + if (MainSub != null) + { + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) { - ContentPackageManager.EnabledPackages.EnableRegular(regular); - GameSettings.SaveCurrentConfig(); + bool savePreviewImage = true; + using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); + try + { + previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); + savePreviewImage = false; + } + MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } - } - if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } - SubmarineInfo.RefreshSavedSub(savePath); - if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } - MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; + else + { + MainSub.TrySaveAs(savePath); + } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { - if (sub.Type != SubmarineType.Player) { continue; } - if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + MainSub.CheckForErrors(); + + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); + + if (savePath.StartsWith(newLocalModDir)) + { + ContentPackageManager.LocalPackages.Refresh(); + var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); + if (newPackage is RegularPackage regular) + { + ContentPackageManager.EnabledPackages.EnableRegular(regular); + GameSettings.SaveCurrentConfig(); + } + } + if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } + SubmarineInfo.RefreshSavedSub(savePath); + if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } + MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; + + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + linkedSubBox.ClearChildren(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } + linkedSubBox.AddItem(sub.Name, sub); + } + subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } - subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } return false; @@ -2408,12 +2445,15 @@ namespace Barotrauma Stretch = true }; var classText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), classGroup.RectTransform), - TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); + TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true) + { + ToolTip = TextManager.Get("submarineclass.description") + }; GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - foreach (SubmarineClass @class in Enum.GetValues(typeof(SubmarineClass))) + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) { - classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{@class}"), @class); + classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{subClass}"), subClass, toolTip: TextManager.Get($"submarineclass.{subClass}.description")); } classDropDown.AddItem(TextManager.Get(nameof(SubmarineTag.Shuttle)), SubmarineTag.Shuttle); classDropDown.OnSelected += (selected, userdata) => @@ -2433,6 +2473,31 @@ namespace Barotrauma }; classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle); + var tierGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), tierGroup.RectTransform), + TextManager.Get("subeditor.tier"), textAlignment: Alignment.CenterLeft, wrap: true) + { + ToolTip = TextManager.Get("submarinetier.description") + }; + + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), tierGroup.RectTransform), NumberType.Int) + { + IntValue = SubmarineInfo.GetDefaultTier(MainSub.Info.Price), + MinValueInt = 1, + MaxValueInt = 3, + OnValueChanged = (numberInput) => + { + MainSub.Info.Tier = numberInput.IntValue; + } + }; + if (MainSub?.Info != null) + { + MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, 3); + } + var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true, @@ -2705,40 +2770,31 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), onUpdate: (f, component) => { - bool canCreateNewPackage = true; foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) { - contentChild.Visible = !(contentChild.UserData is ContentPackage p) - || !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase); - canCreateNewPackage &= contentChild.Visible; contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); } - - if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage) - { - GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); - newPackageListText.Text = TextManager.Get("CreateNewLocalPackage"); - } - if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage) - { - GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton"); - newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text); - } }); - packageToSaveInList.Select(0); ContentPackage ownerPkg = null; if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } foreach (var p in ContentPackageManager.LocalPackages) { - addItemToPackageToSaveList(p.Name, p); + var packageListItem = addItemToPackageToSaveList(p.Name, p); + if (p == ownerPkg) + { + var packageListIcon = packageListItem.GetChild(); + var packageListText = packageListItem.GetChild(); + GUIStyle.Apply(packageListIcon, "WorkshopMenu.EditButton"); + packageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", p.Name); + } } - - if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase)) + if (ownerPkg != null) { - packageToSaveInList.Select(ownerPkg); - packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); + var element = packageToSaveInList.Content.FindChild(ownerPkg); + element?.RectTransform.SetAsFirstChild(); } + packageToSaveInList.Select(0); var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, horizontalArea.RectTransform, Anchor.BottomRight)) @@ -2968,7 +3024,7 @@ namespace Barotrauma return false; } - foreach (char illegalChar in Path.GetInvalidFileNameChars()) + foreach (char illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (nameBox.Text.Contains(illegalChar)) { @@ -3343,23 +3399,21 @@ namespace Barotrauma { if (!(userData is XElement element)) { return; } - #warning TODO: revise +#warning TODO: revise string filePath = element.GetAttributeStringUnrestricted("file", ""); if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); - // set the submarine file path to the "default" value - var unspecifiedFileName = TextManager.Get("UnspecifiedSubFileName"); - loadedSub.Info.FilePath = Path.Combine(ContentPackage.LocalModsDir, unspecifiedFileName.Value, $"{unspecifiedFileName}.sub"); - loadedSub.Info.Name = unspecifiedFileName.Value; try { - loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); + 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); + var unspecifiedFileName = TextManager.Get("UnspecifiedSubFileName"); + loadedSub.Info.Name = unspecifiedFileName.Value; } MainSub = loadedSub; MainSub.SetPrevTransform(MainSub.Position); @@ -3396,7 +3450,8 @@ namespace Barotrauma { if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) { - if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) + if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId) + && publishedWorkshopItemIds.Contains(workshopId.Value)) { AskLoadPublishedSub(selectedSubInfo, workshopPackage); } @@ -3732,7 +3787,6 @@ namespace Barotrauma } else { - List availableLayerOptions = new List { new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) @@ -3775,7 +3829,8 @@ namespace Barotrauma { if (!me.Removed) { me.Remove(); } } - })); + }), + new ContextMenuOption(TextManager.Get("editortip.shiftforextraoptions") + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); } } @@ -4263,7 +4318,7 @@ namespace Barotrauma MapEntity.SelectedList.Clear(); MapEntity.FilteredSelectedList.Clear(); MapEntity.SelectEntity(itemContainer); - dummyCharacter.SelectedConstruction = itemContainer; + dummyCharacter.SelectedItem = itemContainer; FilterEntities(entityFilterBox.Text); } @@ -4274,9 +4329,9 @@ namespace Barotrauma { if (dummyCharacter == null) { return; } //nothing to close -> return - if (DraggedItemPrefab == null && dummyCharacter?.SelectedConstruction == null && OpenedItem == null) { return; } + if (DraggedItemPrefab == null && dummyCharacter?.SelectedItem == null && OpenedItem == null) { return; } DraggedItemPrefab = null; - dummyCharacter.SelectedConstruction = null; + dummyCharacter.SelectedItem = null; OpenedItem?.Drop(dummyCharacter); OpenedItem?.SetTransform(oldItemPosition, 0f); OpenedItem = null; @@ -4353,9 +4408,9 @@ namespace Barotrauma } } - if (dummyCharacter?.SelectedConstruction != null) + if (dummyCharacter?.SelectedItem != null) { - var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + var inv = dummyCharacter?.SelectedItem?.OwnInventory; if (inv != null) { switch (obj) @@ -4785,9 +4840,9 @@ namespace Barotrauma if (dummyCharacter != null) { CharacterHUD.AddToGUIUpdateList(dummyCharacter); - if (dummyCharacter.SelectedConstruction != null) + if (dummyCharacter.SelectedItem != null) { - dummyCharacter.SelectedConstruction.AddToGUIUpdateList(); + dummyCharacter.SelectedItem.AddToGUIUpdateList(); } else if (WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item && item.GetComponent() != null) { @@ -5014,6 +5069,10 @@ namespace Barotrauma SkipInventorySlotUpdate = false; ImageManager.Update((float)deltaTime); +#if DEBUG + Hull.UpdateCheats((float)deltaTime, cam); +#endif + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { saveFrame = null; @@ -5149,7 +5208,7 @@ namespace Barotrauma { if (dummyCharacter != null) { - if (dummyCharacter.SelectedConstruction == null) + if (dummyCharacter.SelectedItem == null) { foreach (var entity in MapEntity.mapEntityList) { @@ -5193,6 +5252,11 @@ namespace Barotrauma } } + if (toggleEntityListBind != GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory]) + { + toggleEntityMenuButton.ToolTip = RichString.Rich($"{TextManager.Get("EntityMenuToggleTooltip")}\n‖color:125,125,125‖{GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].Name}‖color:end‖"); + toggleEntityListBind = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory]; + } if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); @@ -5291,7 +5355,7 @@ namespace Barotrauma me.IsHighlighted = false; } - if (dummyCharacter.SelectedConstruction == null) + if (dummyCharacter.SelectedItem == null) { List wires = new List(); foreach (Item item in Item.ItemList) @@ -5314,8 +5378,8 @@ namespace Barotrauma }); } - if (dummyCharacter.SelectedConstruction == null || - dummyCharacter.SelectedConstruction.GetComponent() != null) + if (dummyCharacter.SelectedItem == null || + dummyCharacter.SelectedItem.GetComponent() != null) { if (WiringMode && PlayerInput.IsShiftDown()) { @@ -5347,7 +5411,7 @@ namespace Barotrauma TeleportDummyCharacter(oldItemPosition); } - if (WiringMode && dummyCharacter?.SelectedConstruction == null) + if (WiringMode && dummyCharacter?.SelectedItem == null) { TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition)); } @@ -5364,7 +5428,7 @@ namespace Barotrauma } // Deposit item from our "infinite stack" into inventory slots - var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + var inv = dummyCharacter?.SelectedItem?.OwnInventory; if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) { var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; @@ -5529,8 +5593,10 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) && - dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) + if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) + && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) + && dummyCharacter?.SelectedItem == null && !WiringMode + && (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero)) { if (layerList is { Visible: true } && GUI.KeyboardDispatcher.Subscriber == layerList) { @@ -5555,9 +5621,9 @@ namespace Barotrauma if (!WiringMode) { - bool shouldCloseHud = dummyCharacter?.SelectedConstruction != null && HUD.CloseHUD(dummyCharacter.SelectedConstruction.Rect) && DraggedItemPrefab == null; + bool shouldCloseHud = dummyCharacter?.SelectedItem != null && HUD.CloseHUD(dummyCharacter.SelectedItem.Rect) && DraggedItemPrefab == null; - if (MapEntityPrefab.Selected != null && GUI.MouseOn == null) + if (MapEntityPrefab.Selected != null) { MapEntityPrefab.Selected.UpdatePlacing(cam); } @@ -5571,7 +5637,7 @@ namespace Barotrauma } else { - if (dummyCharacter?.SelectedConstruction == null) + if (dummyCharacter?.SelectedItem == null) { CreateContextMenu(); } @@ -5628,11 +5694,11 @@ namespace Barotrauma wire?.Update((float)deltaTime, cam); } - if (dummyCharacter.SelectedConstruction != null) + if (dummyCharacter.SelectedItem != null) { - if (MapEntity.SelectedList.Contains(dummyCharacter.SelectedConstruction) || WiringMode) + if (MapEntity.SelectedList.Contains(dummyCharacter.SelectedItem) || WiringMode) { - dummyCharacter.SelectedConstruction?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); + dummyCharacter.SelectedItem?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } else { @@ -5709,7 +5775,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory)); - if (!WiringMode && !IsMouseOnEditorGUI()) + if (!WiringMode) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); MapEntity.DrawSelecting(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 3d95b1bbb..abfbb86e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -1,5 +1,4 @@ #nullable enable -using System; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -14,18 +13,14 @@ using Microsoft.Xna.Framework.Graphics; */ namespace Barotrauma { - class TestScreen : EditorScreen + internal sealed class TestScreen : EditorScreen { public override Camera Cam { get; } private Item? miniMapItem; - private Submarine? submarine; public static Character? dummyCharacter; public static Effect? BlueprintEffect; - private GUIFrame? container; - - private TabMenu? tabMenu; public TestScreen() { @@ -43,14 +38,11 @@ namespace Barotrauma return true; } }; - } public override void Select() { base.Select(); - container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); - var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); if (dummyCharacter is { Removed: false }) { dummyCharacter?.Remove(); @@ -61,30 +53,50 @@ namespace Barotrauma dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); + miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); + + foreach (ItemComponent component in miniMapItem.Components) + { + component.OnItemLoaded(); + } Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); - tabMenu = new TabMenu(); } public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - container?.AddToGUIUpdateList(); - tabMenu?.AddToGUIUpdateList(); - // CharacterHUD.AddToGUIUpdateList(dummyCharacter); - // dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); + CharacterHUD.AddToGUIUpdateList(dummyCharacter); + dummyCharacter?.SelectedItem?.AddToGUIUpdateList(); } public override void Update(double deltaTime) { base.Update(deltaTime); - if (dummyCharacter is { } dummy) + if (dummyCharacter is { } dummy && miniMapItem is { } item) { + if (dummy.SelectedItem != item) + { + dummy.SelectedItem = item; + } + + dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); + Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + + foreach (Limb limb in dummy.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + + if (dummy.AnimController?.Collider is { } collider) + { + collider.SetTransform(pos, 0); + } + dummy.ControlLocalPlayer((float)deltaTime, Cam, false); dummy.Control((float)deltaTime, Cam); } - tabMenu?.Update((float)deltaTime); } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) @@ -93,12 +105,13 @@ namespace Barotrauma graphics.Clear(BackgroundColor); spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: Cam.Transform); - // miniMapItem?.Draw(spriteBatch, false); - // if (dummyCharacter is { } dummy) - // { - // dummyCharacter.DrawFront(spriteBatch, Cam); - // dummyCharacter.Draw(spriteBatch, Cam); - // } + miniMapItem?.Draw(spriteBatch, false); + if (dummyCharacter is { } dummy) + { + dummyCharacter.DrawFront(spriteBatch, Cam); + dummyCharacter.Draw(spriteBatch, Cam); + } + spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 105d82f06..a63e7c905 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -380,12 +380,16 @@ namespace Barotrauma } LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); + if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType) + { + Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description"); + } if (toolTip.IsNullOrEmpty()) { - toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); + toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); } - - if (toolTip == null) + if (toolTip.IsNullOrEmpty()) { toolTip = property.GetAttribute().Description; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 9cf545dc8..8e2172550 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -26,7 +26,9 @@ namespace Barotrauma Gameplay, Mods } - + + public Tab CurrentTab { get; private set; } + private GameSettings.Config unsavedConfig; private readonly GUIFrame mainFrame; @@ -37,7 +39,13 @@ namespace Barotrauma public readonly WorkshopMenu WorkshopMenu; - private static readonly ImmutableHashSet LegacyInputTypes = new List() { InputType.Chat, InputType.RadioChat }.ToImmutableHashSet(); + private static readonly ImmutableHashSet LegacyInputTypes = new List() + { + InputType.Chat, + InputType.RadioChat, + InputType.LocalVoice, + InputType.RadioVoice, + }.ToImmutableHashSet(); public static SettingsMenu Create(RectTransform mainParent) { @@ -97,6 +105,7 @@ namespace Barotrauma public void SelectTab(Tab tab) { + CurrentTab = tab; SwitchContent(tabContents[tab].Content); tabber.Children.ForEach(c => { @@ -220,7 +229,7 @@ namespace Barotrauma }; } - private string Percentage(float v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value; + private string Percentage(float v) => ToolBox.GetFormattedPercentage(v); private int Round(float v) => (int)MathF.Round(v); @@ -764,27 +773,35 @@ namespace Barotrauma private void CreateBottomButtons() { - GUIButton cancelButton = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("Cancel")) + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("Cancel")) + { + OnClicked = (btn, obj) => { - OnClicked = (btn, obj) => - { - Close(); - return false; - } - }; - GUIButton applyButton = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("applysettingsbutton")) + Close(); + return false; + } + }; + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("applysettingsbutton")) + { + OnClicked = (btn, obj) => { - OnClicked = (btn, obj) => + GameSettings.SetCurrentConfig(unsavedConfig); + if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && + mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods) { - GameSettings.SetCurrentConfig(unsavedConfig); - if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) { mutableWorkshopMenu.Apply(); } - GameSettings.SaveCurrentConfig(); - mainFrame.Flash(color: GUIStyle.Green); - return false; + mutableWorkshopMenu.Apply(); } - }; + GameSettings.SaveCurrentConfig(); + mainFrame.Flash(color: GUIStyle.Green); + return false; + }, + OnAddedToGUIUpdateList = (GUIComponent component) => + { + component.Enabled = + CurrentTab != Tab.Mods || + (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods && !mutableWorkshopMenu.ViewingItemDetails); + } + }; } public void Close() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 7249c9972..20f85f5e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -6,7 +6,7 @@ using System.Xml.Linq; namespace Barotrauma.Sounds { - public class OggSound : Sound + class OggSound : Sound { private VorbisReader reader; @@ -49,7 +49,7 @@ namespace Barotrauma.Sounds { if (!muffleFilters.TryGetValue(sampleRate, out BiQuad filter)) { - filter = new LowpassFilter(sampleRate, 800); + filter = new LowpassFilter(sampleRate, 1600); muffleFilters.Add(sampleRate, filter); } filter.Process(buffer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index fbcca4b32..3f4918c57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -6,7 +6,7 @@ using System.Xml.Linq; namespace Barotrauma.Sounds { - public abstract class Sound : IDisposable + abstract class Sound : IDisposable { protected bool disposed; public bool Disposed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs index 79e00f0f1..cacd1121a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs @@ -7,7 +7,7 @@ using System.Text; namespace Barotrauma.Sounds { - public class SoundBuffers : IDisposable + class SoundBuffers : IDisposable { private static readonly HashSet bufferPool = new HashSet(); #if OSX diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 41675864d..bacb39916 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -7,7 +7,7 @@ using System.Diagnostics; namespace Barotrauma.Sounds { - public class SoundSourcePool : IDisposable + class SoundSourcePool : IDisposable { public uint[] ALSources { @@ -80,7 +80,7 @@ namespace Barotrauma.Sounds } } - public class SoundChannel : IDisposable + class SoundChannel : IDisposable { private const int STREAM_BUFFER_SIZE = 8820; private short[] streamShortBuffer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index a2418f455..835981330 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -9,7 +9,7 @@ using Barotrauma.IO; namespace Barotrauma.Sounds { - public class SoundManager : IDisposable + class SoundManager : IDisposable { public const int SOURCE_COUNT = 32; @@ -195,13 +195,13 @@ namespace Barotrauma.Sounds GainMultipliers[index] = gain; } } - private Dictionary categoryModifiers; + + private readonly Dictionary categoryModifiers = new Dictionary(); public SoundManager() { loadedSounds = new List(); streamingThread = null; - categoryModifiers = null; sourcePools = new SoundSourcePool[2]; playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; @@ -235,7 +235,7 @@ namespace Barotrauma.Sounds CompressionDynamicRangeGain = 1.0f; } - private void SetAudioOutputDevice(string deviceName) + private static void SetAudioOutputDevice(string deviceName) { var config = GameSettings.CurrentConfig; config.Audio.AudioOutputDevice = deviceName; @@ -249,7 +249,7 @@ namespace Barotrauma.Sounds DebugConsole.NewMessage($"Attempting to open ALC device \"{deviceName}\""); alcDevice = IntPtr.Zero; - int alcError = Al.NoError; + int alcError; for (int i = 0; i < 3; i++) { alcDevice = Alc.OpenDevice(deviceName); @@ -368,11 +368,13 @@ namespace Barotrauma.Sounds string filePath = overrideFilePath ?? element.GetAttributeContentPath("file")?.Value ?? ""; if (!File.Exists(filePath)) { - throw new System.IO.FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); + throw new System.IO.FileNotFoundException($"Sound file \"{filePath}\" doesn't exist! Content package \"{(element.ContentPackage?.Name ?? "Unknown")}\"."); } - var newSound = new OggSound(this, filePath, stream, xElement: element); - newSound.BaseGain = element.GetAttributeFloat("volume", 1.0f); + var newSound = new OggSound(this, filePath, stream, xElement: element) + { + BaseGain = element.GetAttributeFloat("volume", 1.0f) + }; float range = element.GetAttributeFloat("range", 1000.0f); newSound.BaseNear = range * 0.4f; newSound.BaseFar = range; @@ -537,14 +539,16 @@ namespace Barotrauma.Sounds { if (Disabled) { return; } category = category.ToLower(); - if (categoryModifiers == null) categoryModifiers = new Dictionary(); - if (!categoryModifiers.ContainsKey(category)) + lock (categoryModifiers) { - categoryModifiers.Add(category, new CategoryModifier(index, gain, false)); - } - else - { - categoryModifiers[category].SetGainMultiplier(index, gain); + if (!categoryModifiers.ContainsKey(category)) + { + categoryModifiers.Add(category, new CategoryModifier(index, gain, false)); + } + else + { + categoryModifiers[category].SetGainMultiplier(index, gain); + } } for (int i = 0; i < playingChannels.Length; i++) @@ -562,23 +566,26 @@ namespace Barotrauma.Sounds } } - public float GetCategoryGainMultiplier(string category, int index=-1) + public float GetCategoryGainMultiplier(string category, int index = -1) { if (Disabled) { return 0.0f; } category = category.ToLower(); - if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return 1.0f; - if (index < 0) + lock (categoryModifiers) { - float accumulatedMultipliers = 1.0f; - for (int i = 0; i < categoryModifiers[category].GainMultipliers.Length; i++) + if (categoryModifiers == null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) { return 1.0f; } + if (index < 0) { - accumulatedMultipliers *= categoryModifiers[category].GainMultipliers[i]; + float accumulatedMultipliers = 1.0f; + for (int i = 0; i < categoryModifier.GainMultipliers.Length; i++) + { + accumulatedMultipliers *= categoryModifier.GainMultipliers[i]; + } + return accumulatedMultipliers; + } + else + { + return categoryModifier.GainMultipliers[index]; } - return accumulatedMultipliers; - } - else - { - return categoryModifiers[category].GainMultipliers[index]; } } @@ -587,15 +594,16 @@ namespace Barotrauma.Sounds if (Disabled) { return; } category = category.ToLower(); - - if (categoryModifiers == null) { categoryModifiers = new Dictionary(); } - if (!categoryModifiers.ContainsKey(category)) + lock (categoryModifiers) { - categoryModifiers.Add(category, new CategoryModifier(0, 1.0f, muffle)); - } - else - { - categoryModifiers[category].Muffle = muffle; + if (!categoryModifiers.ContainsKey(category)) + { + categoryModifiers.Add(category, new CategoryModifier(0, 1.0f, muffle)); + } + else + { + categoryModifiers[category].Muffle = muffle; + } } for (int i = 0; i < playingChannels.Length; i++) @@ -618,8 +626,11 @@ namespace Barotrauma.Sounds if (Disabled) { return false; } category = category.ToLower(); - if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) { return false; } - return categoryModifiers[category].Muffle; + lock (categoryModifiers) + { + if (categoryModifiers == null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) { return false; } + return categoryModifier.Muffle; + } } public void Update() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 0705c3084..5870847e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -282,7 +282,7 @@ namespace Barotrauma } else { - if (FlowSounds[i] == null) { continue; } + if (FlowSounds[i]?.Sound == null) { continue; } Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (flowVolumeRight[i] - flowVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (flowSoundChannels[i] == null || !flowSoundChannels[i].IsPlaying) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs index 42422aefa..f462abe24 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs @@ -11,7 +11,7 @@ using Barotrauma.Media; namespace Barotrauma.Sounds { - public class VideoSound : Sound + class VideoSound : Sound { private readonly object mutex; private Queue sampleQueue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 92394a518..7c52cd028 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace Barotrauma.Sounds { - public class VoipSound : Sound + class VoipSound : Sound { public override SoundManager.SourcePoolIndex SourcePoolIndex { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index d4e1648a3..82c294c6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -127,15 +127,22 @@ namespace Barotrauma } } - public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.texture == texture).ToList() : new List() { this }); - - public void ReloadTexture(IEnumerable spritesToUpdate) + public void ReloadTexture() { + var oldTexture = texture; texture.Dispose(); texture = TextureLoader.FromFile(FilePath.Value, Compress); - foreach (Sprite sprite in spritesToUpdate) + Identifier pathKey = FullPath.ToIdentifier(); + if (textureRefCounts.ContainsKey(pathKey)) { - sprite.texture = texture; + textureRefCounts[pathKey].Texture = texture; + } + foreach (Sprite sprite in LoadedSprites) + { + if (sprite.texture == oldTexture) + { + sprite.texture = texture; + } } } @@ -253,12 +260,18 @@ namespace Barotrauma if (flipHorizontal) { float diff = targetSize.X % (sourceRect.Width * scale.X); - flippedDrawOffset.X = (int)((sourceRect.Width * scale.X - diff) / scale.X); + flippedDrawOffset.X = (sourceRect.Width * scale.X - diff) / scale.X; + flippedDrawOffset.X = + MathUtils.NearlyEqual(flippedDrawOffset.X, MathF.Round(flippedDrawOffset.X)) ? + MathF.Round(flippedDrawOffset.X) : flippedDrawOffset.X; } if (flipVertical) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); - flippedDrawOffset.Y = (int)((sourceRect.Height * scale.Y - diff) / scale.Y); + flippedDrawOffset.Y = (sourceRect.Height * scale.Y - diff) / scale.Y; + flippedDrawOffset.Y = + MathUtils.NearlyEqual(flippedDrawOffset.Y, MathF.Round(flippedDrawOffset.Y)) ? + MathF.Round(flippedDrawOffset.Y) : flippedDrawOffset.Y; } drawOffset += flippedDrawOffset; diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index cd908d386..82f0bc176 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; +using Barotrauma.Items.Components; using Barotrauma.Particles; using Barotrauma.Sounds; using Microsoft.Xna.Framework; -using System.Xml.Linq; -using Barotrauma.Items.Components; +using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index ddac26c05..d572c3797 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Steam }); } - internal static void SubscribeToServerMods(IEnumerable missingIds, string rejoinEndpoint, ulong rejoinLobby, string rejoinServerName) + internal static void SubscribeToServerMods(IEnumerable missingIds, ConnectCommand rejoinCommand) { CloseAllMessageBoxes(); GUIMessageBox msgBox = new GUIMessageBox(headerText: "", text: TextManager.Get("PreparingWorkshopDownloads"), @@ -59,9 +59,7 @@ namespace Barotrauma.Steam InitiateDownloads(items, onComplete: () => { ContentPackageManager.UpdateContentPackageList(); - GameMain.Instance.ConnectEndpoint = rejoinEndpoint; - GameMain.Instance.ConnectLobby = rejoinLobby; - GameMain.Instance.ConnectName = rejoinServerName; + GameMain.Instance.ConnectCommand = Option.Some(rejoinCommand); }); }); } @@ -72,7 +70,9 @@ namespace Barotrauma.Steam (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); return (await Task.WhenAll(outOfDatePackages.Where(p => !p.IsUpToDate) - .Select(async p => await SteamManager.Workshop.GetItem(p.Package.SteamWorkshopId)))) + .Select(p => p.Package.UgcId) + .OfType() + .Select(async id => await SteamManager.Workshop.GetItem(id.Value)))) .Where(p => p.HasValue).Select(p => p ?? default).ToArray(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 03760d937..54cab99e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -1,7 +1,5 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -92,7 +90,9 @@ namespace Barotrauma.Steam //currentLobby?.SetData("hostipaddress", lobbyIP); string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); currentLobby?.SetData("pinglocation", pingLocation ?? ""); - currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); + currentLobby?.SetData("lobbyowner", GetSteamId().TryUnwrap(out var steamId) + ? steamId.StringRepresentation + : throw new InvalidOperationException("Steamworks not initialized")); currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); currentLobby?.SetData("message", serverSettings.ServerMessageText); @@ -100,8 +100,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); - currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId))); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); @@ -144,282 +143,12 @@ namespace Barotrauma.Steam lobbyID = (currentLobby?.Id).Value; if (joinServer) { - GameMain.Instance.ConnectLobby = 0; - GameMain.Instance.ConnectName = currentLobby?.GetData("servername"); - GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value); + GameMain.Instance.ConnectCommand = Option.Some( + new ConnectCommand( + currentLobby?.GetData("servername") ?? "Server", + new SteamP2PEndpoint(new SteamId(currentLobby?.Owner.Id ?? 0)))); } }); } - - public static bool GetServers(Action addToServerList, Action serverQueryFinished) - { - if (!IsInitialized) { return false; } - - int doneTasks = 0; - void taskDone() - { - doneTasks++; - if (doneTasks >= 2) - { - serverQueryFinished?.Invoke(); - serverQueryFinished = null; - } - } - - - Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => - { - DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); - }; - - TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), - (t) => - { - Steamworks.Dispatch.OnDebugCallback = null; - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); - taskDone(); - return; - } - var lobbies = ((Task>)t).Result; - if (lobbies != null) - { - foreach (var lobby in lobbies) - { - if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } - - ServerInfo serverInfo = new ServerInfo(); - serverInfo.ServerName = lobby.GetData("name"); - serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); - serverInfo.LobbyID = lobby.Id; - bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); - serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; - serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; - serverInfo.RespondedToSteamQuery = true; - - AssignLobbyDataToServerInfo(lobby, serverInfo); - - addToServerList(serverInfo); - } - } - taskDone(); - }); - - Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); - void onServer(Steamworks.Data.ServerInfo info, bool responsive) - { - if (string.IsNullOrEmpty(info.Name)) { return; } - - ServerInfo serverInfo = new ServerInfo - { - ServerName = info.Name, - HasPassword = info.Passworded, - IP = info.Address.ToString(), - Port = info.ConnectionPort.ToString(), - PlayerCount = info.Players, - MaxPlayers = info.MaxPlayers, - RespondedToSteamQuery = responsive - }; - - if (responsive) - { - TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); - return; - } - - var rules = ((Task>)t).Result; - AssignServerRulesToServerInfo(rules, serverInfo); - - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - }); - } - else - { - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - } - - } - serverQuery.OnResponsiveServer += (info) => onServer(info, true); - serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); - - TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), - (t) => - { - serverQuery.Dispose(); - taskDone(); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); - return; - } - }); - - return true; - } - - public static async Task> LobbyQueryRequest() - { - List allLobbies = new List(); - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() - .FilterDistanceWorldwide() - .WithMaxResults(50); - //steamworks seems to unable to retrieve more than 50 - //lobbies per request; to work around this, we'll make - //up to 10 requests, asking to ignore all previous results - //in each subsequent request - for (int i = 0; i < 10; i++) - { - Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); - if (lobbies == null) { break; } - foreach (var l in lobbies) - { - lobbyQuery = lobbyQuery - .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); - } - allLobbies.AddRange(lobbies); - } - - //make sure all returned lobbies are distinct, don't want any duplicates here - return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); - } - - public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - serverInfo.ServerMessage = lobby.GetData("message"); - serverInfo.GameVersion = lobby.GetData("version"); - - serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); - serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - - string workshopIdData = lobby.GetData("contentpackageid"); - if (!string.IsNullOrEmpty(workshopIdData)) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); - } - else - { - string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - - serverInfo.AllowSpectating = getLobbyBool("allowspectating"); - serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); - serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); - serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); - serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); - if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - - serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier(); - if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - - string pingLocation = lobby.GetData("pinglocation"); - if (!string.IsNullOrEmpty(pingLocation)) - { - serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); - } - - bool? getLobbyBool(string key) - { - string data = lobby.GetData(key); - if (string.IsNullOrEmpty(data)) { return null; } - return data == "True" || data == "true"; - } - } - - public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - if (rules == null) { return; } - - if (rules.ContainsKey("message")) { serverInfo.ServerMessage = rules["message"]; } - if (rules.ContainsKey("version")) { serverInfo.GameVersion = rules["version"]; } - - if (rules.ContainsKey("playercount")) - { - if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; - } - - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - if (rules.ContainsKey("contentpackage")) { serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); } - if (rules.ContainsKey("contentpackagehash")) { serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); } - if (rules.ContainsKey("contentpackageid")) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); - } - else if (rules.ContainsKey("contentpackageurl")) - { - string[] workshopUrls = rules["contentpackageurl"].Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - if (rules.ContainsKey("usingwhitelist")) { serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; } - if (rules.ContainsKey("modeselectionmode")) - { - if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - } - if (rules.ContainsKey("subselectionmode")) - { - if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - } - if (rules.ContainsKey("allowspectating")) { serverInfo.AllowSpectating = rules["allowspectating"] == "True"; } - if (rules.ContainsKey("allowrespawn")) { serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; } - if (rules.ContainsKey("voicechatenabled")) { serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; } - if (rules.ContainsKey("friendlyfireenabled")) { serverInfo.AllowRespawn = rules["friendlyfireenabled"] == "True"; } - if (rules.ContainsKey("karmaenabled")) { serverInfo.VoipEnabled = rules["karmaenabled"] == "True"; } - if (rules.ContainsKey("traitors")) - { - if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - } - - if (rules.ContainsKey("gamestarted")) { serverInfo.GameStarted = rules["gamestarted"] == "True"; } - if (rules.ContainsKey("gamemode")) - { - serverInfo.GameMode = rules["gamemode"].ToIdentifier(); - } - if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) - { - serverInfo.PlayStyle = playStyle; - } - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index e99288885..66463fe83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -28,7 +28,7 @@ namespace Barotrauma.Steam if (IsInitialized) { DebugConsole.NewMessage( - $"Logged in as {GetUsername()} (SteamID {SteamIDUInt64ToString(GetSteamID())})"); + $"Logged in as {GetUsername()} (SteamID {(GetSteamId().TryUnwrap(out var steamId) ? steamId.ToString() : "[NULL]")})"); popularTags.Clear(); int i = 0; @@ -129,7 +129,7 @@ namespace Barotrauma.Steam } - public static bool OverlayCustomURL(string url) + public static bool OverlayCustomUrl(string url) { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { @@ -139,5 +139,10 @@ namespace Barotrauma.Steam Steamworks.SteamFriends.OpenWebOverlay(url); return true; } + + public static void OverlayProfile(SteamId steamId) + { + OverlayCustomUrl($"https://steamcommunity.com/profiles/{steamId.Value}"); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index a21ef24c9..370b47f3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Steam throw new Exception("Expected Workshop package"); } - if (contentPackage.SteamWorkshopId == 0) + if (!contentPackage.UgcId.TryUnwrap(out var ugcId) || !(ugcId is SteamWorkshopId workshopId)) { throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}"); } @@ -210,7 +210,7 @@ namespace Barotrauma.Steam string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; if (File.Exists(newPath) || Directory.Exists(newPath)) { - newPath += $"_{contentPackage.SteamWorkshopId}"; + newPath += $"_{workshopId.Value}"; } if (File.Exists(newPath) || Directory.Exists(newPath)) @@ -226,7 +226,7 @@ namespace Barotrauma.Steam RefreshLocalMods(); - return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId); + return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.UgcId == contentPackage.UgcId); } private struct InstallWaiter @@ -266,7 +266,10 @@ namespace Barotrauma.Steam { NukeDownload(workshopItem); var toUninstall - = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + = ContentPackageManager.WorkshopPackages.Where(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == workshopItem.Id) .ToHashSet(); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); @@ -296,7 +299,10 @@ namespace Barotrauma.Steam return; } else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id) + && !ContentPackageManager.WorkshopPackages.Any(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == id) && !InstallTaskCounter.IsInstalling(id)) { TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 2e5953ae5..cce1627d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -13,6 +13,8 @@ namespace Barotrauma.Steam { private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + public bool ViewingItemDetails { get; private set; } + private readonly GUIDropDown enabledCoreDropdown; private readonly GUIListBox enabledRegularModsList; private readonly GUIListBox disabledRegularModsList; @@ -33,7 +35,12 @@ namespace Barotrauma.Steam memSubscribedModCount = numSubscribedMods; var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); - var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); + var installedIds = ContentPackageManager.WorkshopPackages + .Select(p => p.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value) + .ToHashSet(); foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) { Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); @@ -167,7 +174,7 @@ namespace Barotrauma.Steam swapSoundType = null; } } - + private void CreateInstalledModsTab( out GUIDropDown enabledCoreDropdown, out GUIListBox enabledRegularModsList, @@ -512,7 +519,9 @@ namespace Barotrauma.Steam private void PrepareToShowModInfo(ContentPackage mod) { - TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + if (!mod.UgcId.TryUnwrap(out var ugcId) + || !(ugcId is SteamWorkshopId workshopId)) { return; } + TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), t => { if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } @@ -523,6 +532,7 @@ namespace Barotrauma.Steam public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) { + ViewingItemDetails = false; bulkUpdateButton.Enabled = false; bulkUpdateButton.ToolTip = ""; ContentPackageManager.UpdateContentPackageList(); @@ -589,7 +599,12 @@ namespace Barotrauma.Steam isEnabled: true, onSelected: () => { - TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + var workshopIds = selectedMods + .Select(m => m.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value); + TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(workshopIds.Select(SteamManager.Workshop.GetItem)), t => { if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } @@ -669,7 +684,7 @@ namespace Barotrauma.Steam infoButton.Enabled = false; } TaskPool.AddIfNotFound( - $"DetermineUpdateRequired{mod.SteamWorkshopId}", + $"DetermineUpdateRequired{mod.UgcId}", mod.IsUpToDate(), t => { @@ -722,6 +737,8 @@ namespace Barotrauma.Steam { var mod = child.UserData as RegularPackage; if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } + if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; } + if (!(ugcId is SteamWorkshopId workshopId)) { continue; } var btn = child.GetChild()?.GetAllChildren().Last(); if (btn is null) { continue; } @@ -729,11 +746,11 @@ namespace Barotrauma.Steam btn.ApplyStyle( GUIStyle.GetComponentStyle( - ids.Contains(mod.SteamWorkshopId) + ids.Contains(workshopId.Value) ? "WorkshopMenu.PublishedIcon" : "WorkshopMenu.DownloadedIcon")); btn.ToolTip = TextManager.Get( - ids.Contains(mod.SteamWorkshopId) + ids.Contains(workshopId.Value) ? "PublishedWorkshopMod" : "DownloadedWorkshopMod"); btn.HoverCursor = CursorState.Default; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index e4e155950..17a677d57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -126,7 +126,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - SteamManager.OverlayCustomURL(workshopItem.Url); + SteamManager.OverlayCustomUrl(workshopItem.Url); return false; } }; @@ -226,7 +226,7 @@ namespace Barotrauma.Steam (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems .Select(item => (item, (ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p - => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id))) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == item.Id))) //Sort the pairs by last local edit time if available .OrderBy(t => t.Item2 == null) .ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime) @@ -234,14 +234,16 @@ namespace Barotrauma.Steam int indexOfUserDataInPublishedItemsArray(object userData) => publishedItems.IndexOf(t - => t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)).Id); + => t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)!).Id); //Take the existing GUI items that are in the list and sort to match the order of publishedItems var publishedGuiComponents = selfModsList.Content.Children.OrderBy(c => indexOfUserDataInPublishedItemsArray(c.UserData)).ToArray(); //Get mods that haven't been published and add them to the list var unpublishedMods = ContentPackageManager.LocalPackages - .Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId)) + .Where(p => + !p.TryExtractSteamWorkshopId(out var workshopId) + || !publishedItems.Any(item => item.WorkshopItem.Id == workshopId.Value)) .OrderByDescending(getEditTime).ToArray(); if (unpublishedMods.Any()) @@ -283,6 +285,7 @@ namespace Barotrauma.Steam { CanBeFocused = false }; + unpublishedLayout.Recalculate(); } if (publishedGuiComponents.Any()) @@ -456,6 +459,7 @@ namespace Barotrauma.Steam { CreateSubscribeButton(workshopItem, new RectTransform(Vector2.One, itemLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), spriteScale: 0.4f); } + itemLayout.Recalculate(); } onFill?.Invoke(workshopItems); }); @@ -550,10 +554,13 @@ namespace Barotrauma.Steam private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) { + ViewingItemDetails = true; taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; var contentPackage - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => + p.TryExtractSteamWorkshopId(out var workshopId) + && workshopId.Value == workshopItem.Id); var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); @@ -579,7 +586,7 @@ namespace Barotrauma.Steam SelectedTextColor = GUIStyle.TextColorNormal, OnClicked = (button, o) => { - SteamManager.OverlayCustomURL( + SteamManager.OverlayCustomUrl( $"https://steamcommunity.com/profiles/{author.Id}/myworkshopfiles/?appid={SteamManager.AppID}"); return false; } @@ -616,7 +623,7 @@ namespace Barotrauma.Steam if (contentPackage != null) { TaskPool.AddIfNotFound( - $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", + $"DetermineUpdateRequired{contentPackage.UgcId}", contentPackage.IsUpToDate(), t => { @@ -636,9 +643,10 @@ namespace Barotrauma.Steam new RectTransform(Vector2.Zero, reinstallButton.RectTransform), onUpdate: (f, component) => { - reinstallButton.Visible = workshopItem.IsSubscribed || workshopItem.Owner.Id == SteamManager.GetSteamID(); - reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending && - !SteamManager.Workshop.IsInstalling(workshopItem); + reinstallButton.Visible = workshopItem.IsSubscribed + || workshopItem.Owner.Id == SteamManager.GetSteamId().Select(steamId => steamId.Value).Fallback(0); + reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending + && !SteamManager.Workshop.IsInstalling(workshopItem); reinstallSprite.Color = reinstallButton.Enabled ? reinstallSprite.Style.Color @@ -648,7 +656,9 @@ namespace Barotrauma.Steam if (contentPackage != null && !ContentPackageManager.WorkshopPackages.Contains(contentPackage) - && ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + && ContentPackageManager.WorkshopPackages.Any(p => + p.TryExtractSteamWorkshopId(out var workshopId) + && workshopId.Value == workshopItem.Id)) { updateButton.Visible = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index df5e42de0..daec8758f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Extensions; +using Barotrauma.Steam; using Microsoft.Xna.Framework; namespace Barotrauma @@ -49,21 +50,18 @@ namespace Barotrauma case ModType.Workshop: { var id = element.GetAttributeUInt64("id", 0); - var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); - if (id != 0 && pkg != null) - { - addPkg(pkg); - } + if (id == 0) { continue; } + var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => + p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == id); + if (pkg != null) { addPkg(pkg); } } break; case ModType.Local: { var name = element.GetAttributeString("name", ""); + if (name.IsNullOrEmpty()) { continue; } var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); - if (!name.IsNullOrEmpty() && pkg != null) - { - addPkg(pkg); - } + if (pkg != null) { addPkg(pkg); } } break; } @@ -115,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); + pkgElem.SetAttributeValue("id", pkg.UgcId.ToString()); break; case ModType.Local: pkgElem.SetAttributeValue("name", pkg.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index c8563eb3c..c9cbd489d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -1,12 +1,9 @@ #nullable enable using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; -using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam { @@ -28,6 +25,8 @@ namespace Barotrauma.Steam ShowOnlySubs, ShowOnlyItemAssemblies } + + public Tab CurrentTab { get; private set; } private readonly GUILayoutGroup tabber; private readonly Dictionary tabContents; @@ -78,6 +77,7 @@ namespace Barotrauma.Steam public void SelectTab(Tab tab) { + CurrentTab = tab; SwitchContent(tabContents[tab].Content); tabber.Children.ForEach(c => { @@ -137,7 +137,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - SteamManager.OverlayCustomURL($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); + SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); return false; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 136290f81..61d849dbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -95,6 +95,9 @@ namespace Barotrauma.Steam SelectTab(Tab.Publish); } + private static bool PackageMatchesItem(ContentPackage p, Steamworks.Ugc.Item workshopItem) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == workshopItem.Id; + private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); @@ -105,18 +108,19 @@ namespace Barotrauma.Steam childAnchor: Anchor.TopCenter); Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default; + ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package) ? package - : ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + : ContentPackageManager.LocalPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); ContentPackage? workshopPackage - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); if (localPackage is null) { new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null); //Local copy does not exist; check for Workshop copy bool workshopCopyExists = - ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem)); new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform), TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"), @@ -403,7 +407,7 @@ namespace Barotrauma.Steam private IEnumerable CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) { ContentPackage? workshopCopy = - ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); if (workshopCopy is null) { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) @@ -417,7 +421,7 @@ namespace Barotrauma.Steam { ContentPackageManager.WorkshopPackages.Refresh(); }); - while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + while (!ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem))) { currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) ? TextManager.Get("PublishPopupInstall") @@ -426,7 +430,7 @@ namespace Barotrauma.Steam } workshopCopy = - ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.First(p => PackageMatchesItem(p, workshopItem)); } bool localCopyMade = false; @@ -480,7 +484,7 @@ namespace Barotrauma.Steam messageBox.Buttons[0].Enabled = false; Steamworks.Ugc.PublishResult? result = null; Exception? resultException = null; - TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", + TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.UgcId})", editor.SubmitAsync(), t => { @@ -496,6 +500,8 @@ namespace Barotrauma.Steam if (result is { Success: true }) { var resultId = result.Value.FileId; + bool packageMatchesResult(ContentPackage p) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == resultId; Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem); while (!resultItem.IsInstalled && !downloadTask.IsCompleted) @@ -511,7 +517,7 @@ namespace Barotrauma.Steam } ContentPackage? pkgToNuke - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(packageMatchesResult); if (pkgToNuke != null) { Directory.Delete(pkgToNuke.Dir, recursive: true); @@ -537,7 +543,7 @@ namespace Barotrauma.Steam var localModProject = new ModProject(localPackage) { - SteamWorkshopId = resultId + UgcId = Option.Some(new SteamWorkshopId(resultId)) }; localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); @@ -546,7 +552,7 @@ namespace Barotrauma.Steam if (result.Value.NeedsWorkshopAgreement) { - SteamManager.OverlayCustomURL(resultItem.Url); + SteamManager.OverlayCustomUrl(resultItem.Url); } new GUIMessageBox(string.Empty, TextManager.GetWithVariable("workshopitempublished", "[itemname]", localPackage.Name)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs index f13fdd117..f2cf800d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs @@ -19,7 +19,7 @@ namespace Barotrauma public override bool Loaded => nestedStr.Loaded; public override void RetrieveValue() { - cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.Value, textScale); + cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.GetFontForStr(nestedStr.Value), textScale); UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs new file mode 100644 index 000000000..fb86070c6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs @@ -0,0 +1,32 @@ +#nullable enable +using Barotrauma.Networking; + +namespace Barotrauma +{ + readonly struct ConnectCommand + { + public readonly struct NameAndEndpoint + { + public readonly string ServerName; + public readonly Endpoint Endpoint; + + public NameAndEndpoint(string serverName, Endpoint endpoint) + { + ServerName = serverName; + Endpoint = endpoint; + } + } + + public readonly Either EndpointOrLobby; + + public ConnectCommand(string serverName, Endpoint endpoint) + { + EndpointOrLobby = new NameAndEndpoint(serverName, endpoint); + } + + public ConnectCommand(ulong lobbyId) + { + EndpointOrLobby = lobbyId; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index b4662d858..f1fc1456d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -1,30 +1,75 @@ #if DEBUG +using Barotrauma.IO; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Text; -using System.Text.RegularExpressions; using System.Linq; -using System.Globalization; +using System.Text; namespace Barotrauma { class LocalizationCSVtoXML { - private static Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"])*\"|[^,]*)", RegexOptions.Compiled); // Handling commas inside data fields surrounded by "" - private static List conversationClosingIndent = new List(); - private static char[] separator = new char[1] { '|' }; + private static readonly List conversationClosingIndent = new List(); + private static readonly char[] separator = new char[1] { '|' }; private const string conversationsPath = "Content/NPCConversations"; private const string infoTextPath = "Content/Texts"; private const string xmlHeader = ""; - private static string[,] translatedLanguageNames = new string[13, 2] { { "English", "English" }, { "French", "Français" }, { "German", "Deutsch" }, + private static readonly string[,] translatedLanguageNames = new string[13, 2] { { "English", "English" }, { "French", "Français" }, { "German", "Deutsch" }, { "Russian", "Русский" }, { "Brazilian Portuguese", "Português brasileiro" }, { "Simplified Chinese", "中文(简体)" }, { "Traditional Chinese", "中文(繁體)" }, { "Castilian Spanish", "Castellano" }, { "Latinamerican Spanish", "Español Latinoamericano" }, { "Polish", "Polski" }, { "Turkish", "Türkçe" }, { "Japanese", "日本語" }, { "Korean", "한국어" } }; - public static void Convert() + public static void ConvertMasterLocalizationKit(string outputTextsDirectory, string outputConversationsDirectory, bool convertConversations) + { + string textFilePath = Path.Combine(infoTextPath, "Texts.csv"); + string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); + + Dictionary> xmlContent; + try + { + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); + } + catch (Exception e) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); + return; + } + if (xmlContent == null) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); + return; + } + foreach (string language in xmlContent.Keys) + { + string languageNoWhitespace = language.Replace(" ", ""); + string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}Vanilla.xml"); + File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); + DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + } + + if (convertConversations) + { + var conversationLinesAll = File.ReadAllLines(conversationFilePath, Encoding.UTF8); + foreach (string language in xmlContent.Keys) + { + List convXmlContent = ConvertConversationsToXML(conversationLinesAll, language); + if (convXmlContent == null) + { + DebugConsole.ThrowError("NPCConversation Localization .csv to .xml conversion failed for: " + language); + continue; + } + string languageNoWhitespace = language.Replace(" ", ""); + string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"NpcConversations_{languageNoWhitespace}.xml"); + File.WriteAllLines(xmlFileFullPath, convXmlContent, Encoding.UTF8); + DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath); + } + } + } + + [Obsolete] + public static void ConvertIndividualFiles() { if (GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage) { @@ -89,8 +134,7 @@ namespace Barotrauma for (int j = 0; j < infoTextFiles.Count; j++) { - - List xmlContent = null; + List xmlContent; try { xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); @@ -121,6 +165,109 @@ namespace Barotrauma } } + private static Dictionary> ConvertInfoTextToXML(string[] csvContent) + { + Dictionary> xmlContentByLanguage = new Dictionary>(); + + //get all the languages from the header row + string headerRow = csvContent[0]; + var headerContent = headerRow.Split(separator); + for (int i = 0; i < headerContent.Length; i++) + { + string languageName = headerContent[i]; + if (languageName.Equals("tag", StringComparison.OrdinalIgnoreCase) || + languageName.Equals("comments", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string translatedName = GetTranslatedName(languageName); + bool nowhitespace = TextManager.IsCJK(translatedName); + List xmlContent = new List() + { + xmlHeader, + $"" + }; + xmlContentByLanguage.Add(headerContent[i], xmlContent); + } + + for (int row = 1; row < csvContent.Length; row++) // Start at one to ignore header + { + if (!xmlContentByLanguage.Values.All(values => values.Count == xmlContentByLanguage["English"].Count)) + { + throw new Exception($"Error while converting csv to xml: mismatching number of texts on line {row-1} ({csvContent[row - 1]}). Check that there's no extra newlines, separators or missing lines in the csv file."); + } + + if (csvContent[row].Length == 0) + { + AddToAllLanguages(string.Empty); + } + else + { + string[] split = csvContent[row].Split(separator); + + if (split.Length < xmlContentByLanguage.Count) + { + throw new Exception($"Error while converting csv to xml: not enough values on line {row} ({csvContent[row]}). Check that there's no extra newlines, separators or missing lines in the csv file."); + } + + if (split.Length > 1) // Localization data + { + //all values empty = an empty line + if (split.All(s => s.IsNullOrEmpty())) + { + AddToAllLanguages(string.Empty); + } + //value is empty in all languages + else if (!split[0].IsNullOrEmpty() && split.Skip(2).All(s => s.IsNullOrEmpty())) + { + //first line is all lower-case and contains dot, assume it's an empty value + if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) + { + AddToAllLanguages($"<{split[0]}>"); + } + //otherwise assume it's a comment + else + { + AddToAllLanguages($""); + } + } + else + { + for (int j = 0; j < split.Length; j++) + { + string languageName = headerContent[j]; + if (languageName.Equals("tag", StringComparison.OrdinalIgnoreCase) || + languageName.Equals("comments", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + split[j] = split[j].Replace(" & ", " & "); + xmlContentByLanguage[languageName].Add($"<{split[0]}>{split[j]}"); + } + } + } + else // A header/comment + { + AddToAllLanguages($""); + } + } + } + + AddToAllLanguages(string.Empty); + AddToAllLanguages(""); + + void AddToAllLanguages(string str) + { + foreach (var xmlContent in xmlContentByLanguage.Values) + { + xmlContent.Add(str); + } + } + + return xmlContentByLanguage; + } + + [Obsolete] private static List ConvertInfoTextToXML(string[] csvContent, string language) { List xmlContent = new List @@ -147,12 +294,6 @@ namespace Barotrauma if (split.Length >= 2) // Localization data { - if (split.Length > 2 && !split[0].All(char.IsLower)) // Invalid header in line with localization data - { - split[0] = split[1]; - split[1] = split[2]; - split[2] = string.Empty; - } split[1] = split[1].Replace(" & ", " & "); xmlContent.Add($"<{split[0]}>{split[1]}"); } @@ -186,68 +327,39 @@ namespace Barotrauma private static List ConvertConversationsToXML(string[] csvContent, string language) { - List xmlContent = new List(); - xmlContent.Add(xmlHeader); + List xmlContent = new List + { + xmlHeader + }; string translatedName = GetTranslatedName(language); bool nowhitespace = TextManager.IsCJK(translatedName); + int languageColumn = -1; + string[] headerSplit = csvContent[0].Split(separator); + for (int i = 0; i < headerSplit.Length; i++) + { + if (headerSplit[i] == language) + { + languageColumn = i; + break; + } + } + xmlContent.Add($""); - xmlContent.Add(string.Empty); - xmlContent.Add(""); - int traitStart = -1; - for (int i = 0; i < csvContent.Length; i++) - { - if (csvContent[i].StartsWith("Personality")) - { - traitStart = i + 1; - break; - } - } - - int conversationStart = -1; - for (int i = 0; i < csvContent.Length; i++) - { - if (csvContent[i].StartsWith("Generic")) - { - conversationStart = i; - break; - } - } - - if (traitStart == -1) - { - DebugConsole.ThrowError("Invalid formatting of NPCConversations, no traits found!"); - return null; - } - - //DebugConsole.NewMessage("Count: " + NPCPersonalityTrait.List.Count); - var traits = NPCPersonalityTrait.GetAll(language.ToLanguageIdentifier()).ToArray(); - for (int i = 0; i < traits.Length; i++) // Traits - { - //string[] split = SplitCSV(csvContent[traitStart + i].Trim(separator)); - string[] split = csvContent[traitStart + i].Split(separator); - xmlContent.Add( - $""); - } + int conversationStart = 1; xmlContent.Add(string.Empty); for (int i = conversationStart; i < csvContent.Length; i++) // Conversations { string[] split = csvContent[i].Split(separator); - int emptyFields = 0; - for (int j = 0; j < split.Length; j++) { - if (split[j] == string.Empty) emptyFields++; + if (split[j] == string.Empty) { emptyFields++; } } - if (emptyFields == split.Length) // Empty line with only commas, indicates the end of the previous conversation { HandleClosingElements(xmlContent, 0); @@ -260,10 +372,10 @@ namespace Barotrauma continue; } - string speaker = split[1]; - int depthIndex = int.Parse(split[2]); + string line = split[languageColumn].Replace("\"", ""); + string speaker = split[2]; + int depthIndex = int.Parse(split[3]); // 3 = original line - string line = split[3].Replace("\"", ""); string flags = split[4].Replace("\"", ""); string allowedJobs = split[5].Replace("\"", ""); string speakerTags = split[6].Replace("\"", ""); @@ -317,7 +429,7 @@ namespace Barotrauma xmlContent.Add(""); return xmlContent; - } + } private static void HandleClosingElements(List xmlContent, int targetDepth) { @@ -332,24 +444,6 @@ namespace Barotrauma } } - private static string[] SplitCSV(string input) // Splits the .csv with regex, leaving commas inside quotation marks intact - { - List list = new List(); - string curr = null; - foreach (Match match in csvSplit.Matches(input)) - { - curr = match.Value; - if (0 == curr.Length) - { - list.Add(""); - } - - list.Add(curr.TrimStart(separator)); - } - - return list.ToArray(); - } - private static string GetIndenting(int depthIndex) { string indenting = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/RichTextData.cs new file mode 100644 index 000000000..19ce051e9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/RichTextData.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + static class RichTextDataExtensions + { + public static Client ExtractClient(this RichTextData data) + { + bool isInt = UInt64.TryParse(data.Metadata, out ulong uintId); + Option accountId = AccountId.Parse(data.Metadata); + Client client = GameMain.Client.ConnectedClients.Find(c => accountId.IsSome() && accountId == c.AccountId) + ?? GameMain.Client.ConnectedClients.Find(c => isInt && c.SessionId == uintId) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => accountId.IsSome() && accountId == c.AccountId) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => isInt && c.SessionId == uintId); + return client; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index 6fdfc7098..43ba00158 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -7,7 +7,7 @@ using System.Text; namespace Barotrauma { - public class SpriteRecorder : ISpriteBatch, IDisposable + class SpriteRecorder : ISpriteBatch, IDisposable { private struct Command { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index c11566a30..3d193471b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -4,11 +4,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using Barotrauma.Networking; using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma { - public static partial class ToolBox + static partial class ToolBox { /// /// Checks if point is inside of a polygon @@ -450,21 +451,26 @@ namespace Barotrauma public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) => font.WrapText(text, lineLength / textScale); - public static void ParseConnectCommand(string[] args, out string name, out string endpoint, out UInt64 lobbyId) + public static Option ParseConnectCommand(string[] args) { - name = null; endpoint = null; lobbyId = 0; - if (args == null || args.Length < 2) { return; } + if (args == null || args.Length < 2) { return Option.None(); } if (args[0].Equals("-connect", StringComparison.OrdinalIgnoreCase)) { - if (args.Length < 3) { return; } - name = args[1]; - endpoint = args[2]; + if (args.Length < 3) { return Option.None(); } + if (!(Endpoint.Parse(args[2]).TryUnwrap(out var endpoint))) { return Option.None(); } + return Option.Some( + new ConnectCommand( + serverName: args[1], + endpoint: endpoint)); } else if (args[0].Equals("+connect_lobby", StringComparison.OrdinalIgnoreCase)) { - UInt64.TryParse(args[1], out lobbyId); + return UInt64.TryParse(args[1], out var lobbyId) + ? Option.Some(new ConnectCommand(lobbyId)) + : Option.None(); } + return Option.None(); } public static bool VersionNewerIgnoreRevision(Version a, Version b) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index e0ac1dffb..11d281dce 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -7,39 +7,31 @@ WinExe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 en - - DEBUG;TRACE;CLIENT;LINUX;USE_STEAM x64 ..\bin\$(Configuration)Linux\ - 8 TRACE;DEBUG;CLIENT;LINUX;X64;USE_STEAM x64 ..\bin\$(Configuration)Linux\ - 8 @@ -69,7 +61,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 30b6ad713..3040617a3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -7,16 +7,17 @@ WinExe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 false en @@ -63,7 +64,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 200e77b7b..c44c559d3 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -7,16 +7,17 @@ WinExe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true app.manifest ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 Doc\BuildDocClient.xml @@ -69,7 +70,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 97aca5a84..717ebde2c 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -1,45 +1,31 @@ - - - - - Exe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - en - - DEBUG;TRACE;SERVER;LINUX;USE_STEAM x64 ..\bin\$(Configuration)Linux\ - 8 TRACE;DEBUG;SERVER;LINUX;X64;USE_STEAM x64 ..\bin\$(Configuration)Linux\ - 8 @@ -69,7 +55,7 @@ - + @@ -158,4 +144,4 @@ - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 90d94a8cf..dc3dd32d7 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -1,24 +1,19 @@ - - - - - Exe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - en @@ -64,7 +59,7 @@ - + @@ -161,4 +156,4 @@ - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 197c22a26..49893f4dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -47,42 +46,40 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg) { - msg.Write(ID); - msg.Write(Name); - msg.Write(OriginalName); - msg.Write((byte)Head.Preset.TagSet.Count); + msg.WriteUInt16(ID); + msg.WriteString(Name); + msg.WriteString(OriginalName); + msg.WriteByte((byte)Head.Preset.TagSet.Count); foreach (Identifier tag in Head.Preset.TagSet) { - msg.Write(tag); + msg.WriteIdentifier(tag); } - msg.Write((byte)Head.HairIndex); - msg.Write((byte)Head.BeardIndex); - msg.Write((byte)Head.MoustacheIndex); - msg.Write((byte)Head.FaceAttachmentIndex); + msg.WriteByte((byte)Head.HairIndex); + msg.WriteByte((byte)Head.BeardIndex); + msg.WriteByte((byte)Head.MoustacheIndex); + msg.WriteByte((byte)Head.FaceAttachmentIndex); msg.WriteColorR8G8B8(Head.SkinColor); msg.WriteColorR8G8B8(Head.HairColor); msg.WriteColorR8G8B8(Head.FacialHairColor); - msg.Write(ragdollFileName); + msg.WriteString(ragdollFileName); + msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); if (Job != null) { - msg.Write(Job.Prefab.Identifier); - msg.Write((byte)Job.Variant); - var skills = Job.GetSkills(); - msg.Write((byte)skills.Count()); - foreach (Skill skill in skills) + msg.WriteUInt32(Job.Prefab.UintIdentifier); + msg.WriteByte((byte)Job.Variant); + foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) { - msg.Write(skill.Identifier); - msg.Write(skill.Level); + msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier).Level); } } else { - msg.Write(""); - msg.Write((byte)0); + msg.WriteUInt32((uint)0); + msg.WriteByte((byte)0); } - msg.Write((ushort)ExperiencePoints); + msg.WriteUInt16((ushort)ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index ff4ac2e44..a6357e6ed 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -8,7 +8,7 @@ namespace Barotrauma { partial class Character { - public string OwnerClientEndPoint; + public Address OwnerClientAddress; public string OwnerClientName; public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -305,25 +305,25 @@ namespace Barotrauma public void ServerWritePosition(IWriteMessage msg, Client c) { - msg.Write(ID); + msg.WriteUInt16(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); if (this == c.Character) { - tempBuffer.Write(true); + tempBuffer.WriteBoolean(true); if (LastNetworkUpdateID < memInput.Count + 1) { - tempBuffer.Write((UInt16)0); + tempBuffer.WriteUInt16((UInt16)0); } else { - tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); + tempBuffer.WriteUInt16((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); } } else { - tempBuffer.Write(false); + tempBuffer.WriteBoolean(false); bool aiming = false; bool use = false; @@ -346,40 +346,41 @@ namespace Barotrauma networkUpdateSent = true; } - tempBuffer.Write(aiming); - tempBuffer.Write(shoot); - tempBuffer.Write(use); + tempBuffer.WriteBoolean(aiming); + tempBuffer.WriteBoolean(shoot); + tempBuffer.WriteBoolean(use); if (AnimController is HumanoidAnimController) { - tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + tempBuffer.WriteBoolean(((HumanoidAnimController)AnimController).Crouching); } - tempBuffer.Write(attack); + tempBuffer.WriteBoolean(attack); Vector2 relativeCursorPos = cursorPosition - AimRefPosition; - tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); + tempBuffer.WriteUInt16((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); - tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); + tempBuffer.WriteBoolean(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); - tempBuffer.Write(AnimController.Dir > 0.0f); + tempBuffer.WriteBoolean(AnimController.Dir > 0.0f); } - if (SelectedCharacter != null || SelectedConstruction != null) + if (SelectedCharacter != null || HasSelectedAnyItem) { - tempBuffer.Write(true); - tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); - tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID); + tempBuffer.WriteBoolean(true); + tempBuffer.WriteUInt16(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); + tempBuffer.WriteUInt16(SelectedItem != null ? SelectedItem.ID : NullEntityID); + tempBuffer.WriteUInt16(SelectedSecondaryItem != null ? SelectedSecondaryItem.ID : NullEntityID); if (SelectedCharacter != null) { - tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR); + tempBuffer.WriteBoolean(AnimController.Anim == AnimController.Animation.CPR); } } else { - tempBuffer.Write(false); + tempBuffer.WriteBoolean(false); } - tempBuffer.Write(SimPosition.X); - tempBuffer.Write(SimPosition.Y); + tempBuffer.WriteSingle(SimPosition.X); + tempBuffer.WriteSingle(SimPosition.Y); float MaxVel = NetConfig.MaxPhysicsBodyVelocity; AnimController.Collider.LinearVelocity = new Vector2( MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), @@ -388,17 +389,17 @@ namespace Barotrauma tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; - tempBuffer.Write(fixedRotation); + tempBuffer.WriteBoolean(fixedRotation); if (!fixedRotation) { - tempBuffer.Write(AnimController.Collider.Rotation); + tempBuffer.WriteSingle(AnimController.Collider.Rotation); float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); } bool writeStatus = healthUpdateTimer <= 0.0f; - tempBuffer.Write(writeStatus); + tempBuffer.WriteBoolean(writeStatus); if (writeStatus) { WriteStatus(tempBuffer); @@ -409,7 +410,7 @@ namespace Barotrauma tempBuffer.WritePadBits(); msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) @@ -420,30 +421,30 @@ namespace Barotrauma switch (eventData) { case InventoryStateEventData _: - msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); + msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerEventWrite(msg, c); break; case ControlEventData controlEventData: Client owner = controlEventData.Owner; - msg.Write(owner == c && owner.Character == this); - msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); + msg.WriteBoolean(owner == c && owner.Character == this); + msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0); break; - case CharacterStatusEventData _: - WriteStatus(msg); + case CharacterStatusEventData statusEventData: + WriteStatus(msg, statusEventData.ForceAfflictionData); break; case UpdateSkillsEventData _: if (Info?.Job == null) { - msg.Write((byte)0); + msg.WriteByte((byte)0); } else { var skills = Info.Job.GetSkills(); - msg.Write((byte)skills.Count()); + msg.WriteByte((byte)skills.Count()); foreach (Skill skill in skills) { - msg.Write(skill.Identifier); - msg.Write(skill.Level); + msg.WriteIdentifier(skill.Identifier); + msg.WriteSingle(skill.Level); } } break; @@ -460,33 +461,33 @@ namespace Barotrauma targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb); } } - msg.Write((byte)(attackLimbIndex < 0 ? 255 : attackLimbIndex)); - msg.Write((ushort)targetEntityId); - msg.Write((byte)(targetLimbIndex < 0 ? 255 : targetLimbIndex)); - msg.Write(attackEventData.TargetSimPos.X); - msg.Write(attackEventData.TargetSimPos.Y); + msg.WriteByte((byte)(attackLimbIndex < 0 ? 255 : attackLimbIndex)); + msg.WriteUInt16((ushort)targetEntityId); + msg.WriteByte((byte)(targetLimbIndex < 0 ? 255 : targetLimbIndex)); + msg.WriteSingle(attackEventData.TargetSimPos.X); + msg.WriteSingle(attackEventData.TargetSimPos.Y); } break; case AssignCampaignInteractionEventData _: - msg.Write((byte)CampaignInteractionType); - msg.Write(RequireConsciousnessForCustomInteract); + msg.WriteByte((byte)CampaignInteractionType); + msg.WriteBoolean(RequireConsciousnessForCustomInteract); break; case ObjectiveManagerStateEventData objectiveManagerStateEventData: AIObjectiveManager.ObjectiveType type = objectiveManagerStateEventData.ObjectiveType; msg.WriteRangedInteger((int)type, (int)AIObjectiveManager.ObjectiveType.MinValue, (int)AIObjectiveManager.ObjectiveType.MaxValue); if (!(AIController is HumanAIController controller)) { - msg.Write(false); + msg.WriteBoolean(false); break; } if (type == AIObjectiveManager.ObjectiveType.Order) { var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); bool validOrder = currentOrderInfo != null; - msg.Write(validOrder); + msg.WriteBoolean(validOrder); if (!validOrder) { break; } var orderPrefab = currentOrderInfo.Prefab; - msg.Write(orderPrefab.UintIdentifier); + msg.WriteUInt32(orderPrefab.UintIdentifier); if (!orderPrefab.HasOptions) { break; } int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); if (optionIndex == -1) @@ -499,65 +500,62 @@ namespace Barotrauma { var objective = controller.ObjectiveManager.CurrentObjective; bool validObjective = objective?.Identifier is { IsEmpty: false }; - msg.Write(validObjective); + msg.WriteBoolean(validObjective); if (!validObjective) { break; } - msg.Write(objective.Identifier); - msg.Write(objective.Option); + msg.WriteIdentifier(objective.Identifier); + msg.WriteIdentifier(objective.Option); UInt16 targetEntityId = 0; if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) { targetEntityId = operateObjective.OperateTarget.ID; } - msg.Write(targetEntityId); + msg.WriteUInt16(targetEntityId); } break; case TeamChangeEventData _: - msg.Write((byte)TeamID); + msg.WriteByte((byte)TeamID); break; case AddToCrewEventData addToCrewEventData: - msg.Write((byte)addToCrewEventData.TeamType); // team id - ushort[] inventoryItemIDs = addToCrewEventData.InventoryItems.Select(item => item.ID).ToArray(); - msg.Write((ushort)inventoryItemIDs.Length); - for (int i = 0; i < inventoryItemIDs.Length; i++) - { - msg.Write(inventoryItemIDs[i]); - } + msg.WriteNetSerializableStruct(addToCrewEventData.ItemTeamChange); + break; + case RemoveFromCrewEventData removeFromCrewEventData: + msg.WriteNetSerializableStruct(removeFromCrewEventData.ItemTeamChange); break; case UpdateExperienceEventData _: - msg.Write(Info.ExperiencePoints); + msg.WriteInt32(Info.ExperiencePoints); break; case UpdateTalentsEventData _: - msg.Write((ushort)characterTalents.Count); + msg.WriteUInt16((ushort)characterTalents.Count); foreach (var unlockedTalent in characterTalents) { - msg.Write(unlockedTalent.AddedThisRound); - msg.Write(unlockedTalent.Prefab.UintIdentifier); + msg.WriteBoolean(unlockedTalent.AddedThisRound); + msg.WriteUInt32(unlockedTalent.Prefab.UintIdentifier); } break; case UpdateMoneyEventData _: - msg.Write(GameMain.GameSession.Campaign.GetWallet(c).Balance); + msg.WriteInt32(GameMain.GameSession.Campaign.GetWallet(c).Balance); break; case UpdatePermanentStatsEventData updatePermanentStatsEventData: StatTypes statType = updatePermanentStatsEventData.StatType; if (Info == null) { - msg.Write((byte)0); - msg.Write((byte)0); + msg.WriteByte((byte)0); + msg.WriteByte((byte)0); } else if (!Info.SavedStatValues.ContainsKey(statType)) { - msg.Write((byte)0); - msg.Write((byte)statType); + msg.WriteByte((byte)0); + msg.WriteByte((byte)statType); } else { - msg.Write((byte)Info.SavedStatValues[statType].Count); - msg.Write((byte)statType); + msg.WriteByte((byte)Info.SavedStatValues[statType].Count); + msg.WriteByte((byte)statType); foreach (var savedStatValue in Info.SavedStatValues[statType]) { - msg.Write(savedStatValue.StatIdentifier); - msg.Write(savedStatValue.StatValue); - msg.Write(savedStatValue.RemoveOnDeath); + msg.WriteString(savedStatValue.StatIdentifier); + msg.WriteSingle(savedStatValue.StatValue); + msg.WriteBoolean(savedStatValue.RemoveOnDeath); } } break; @@ -571,15 +569,15 @@ namespace Barotrauma /// Normally full affliction data is not written for dead characters, this can be used to force them to be written private void WriteStatus(IWriteMessage msg, bool forceAfflictionData = false) { - msg.Write(IsDead); + msg.WriteBoolean(IsDead); if (IsDead) { msg.WriteRangedInteger((int)CauseOfDeath.Type, 0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1); if (CauseOfDeath.Type == CauseOfDeathType.Affliction) { - msg.Write(CauseOfDeath.Affliction.Identifier); + msg.WriteUInt32(CauseOfDeath.Affliction.UintIdentifier); } - msg.Write(forceAfflictionData); + msg.WriteBoolean(forceAfflictionData); if (forceAfflictionData) { CharacterHealth.ServerWrite(msg); @@ -592,7 +590,7 @@ namespace Barotrauma if (AnimController?.LimbJoints == null) { //0 limbs severed - msg.Write((byte)0); + msg.WriteByte((byte)0); } else { @@ -604,10 +602,10 @@ namespace Barotrauma severedJointIndices.Add(i); } } - msg.Write((byte)severedJointIndices.Count); + msg.WriteByte((byte)severedJointIndices.Count); foreach (int jointIndex in severedJointIndices) { - msg.Write((byte)jointIndex); + msg.WriteByte((byte)jointIndex); } } } @@ -618,23 +616,24 @@ namespace Barotrauma int initialMsgLength = msg.LengthBytes; - msg.Write(Info == null); - msg.Write(entityId); - msg.Write(SpeciesName); - msg.Write(Seed); + msg.WriteBoolean(Info == null); + msg.WriteUInt16(entityId); + msg.WriteIdentifier(SpeciesName); + msg.WriteString(Seed); if (Removed) { - msg.Write(0.0f); - msg.Write(0.0f); + msg.WriteSingle(0.0f); + msg.WriteSingle(0.0f); } else { - msg.Write(WorldPosition.X); - msg.Write(WorldPosition.Y); + msg.WriteSingle(WorldPosition.X); + msg.WriteSingle(WorldPosition.Y); } - msg.Write(Enabled); + msg.WriteBoolean(Enabled); + msg.WriteBoolean(DisabledByEvent); //character with no characterinfo (e.g. some monster) if (Info == null) @@ -646,54 +645,54 @@ namespace Barotrauma Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); if (ownerClient != null) { - msg.Write(true); - msg.Write(ownerClient.ID); + msg.WriteBoolean(true); + msg.WriteByte(ownerClient.SessionId); } else if (GameMain.Server.Character == this) { - msg.Write(true); - msg.Write((byte)0); + msg.WriteBoolean(true); + msg.WriteByte((byte)0); } else { - msg.Write(false); + msg.WriteBoolean(false); } - msg.Write(HumanPrefabHealthMultiplier); - msg.Write(Wallet.Balance); + msg.WriteSingle(HumanPrefabHealthMultiplier); + msg.WriteInt32(Wallet.Balance); msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); - msg.Write((byte)TeamID); - msg.Write(this is AICharacter); - msg.Write(info.SpeciesName); + msg.WriteByte((byte)TeamID); + msg.WriteBoolean(this is AICharacter); + msg.WriteIdentifier(info.SpeciesName); int msgLengthBeforeInfo = msg.LengthBytes; info.ServerWrite(msg); int infoLength = msg.LengthBytes - msgLengthBeforeInfo; - msg.Write((byte)CampaignInteractionType); + msg.WriteByte((byte)CampaignInteractionType); if (CampaignInteractionType == CampaignMode.InteractionType.Store) { - msg.Write(MerchantIdentifier); + msg.WriteIdentifier(MerchantIdentifier); } int msgLengthBeforeOrders = msg.LengthBytes; // Current orders - msg.Write((byte)info.CurrentOrders.Count(o => o != null)); + msg.WriteByte((byte)info.CurrentOrders.Count(o => o != null)); foreach (var orderInfo in info.CurrentOrders) { if (orderInfo == null) { continue; } - msg.Write(orderInfo.Prefab.UintIdentifier); - msg.Write(orderInfo.TargetEntity == null ? (UInt16)0 : orderInfo.TargetEntity.ID); + msg.WriteUInt32(orderInfo.Prefab.UintIdentifier); + msg.WriteUInt16(orderInfo.TargetEntity == null ? (UInt16)0 : orderInfo.TargetEntity.ID); var hasOrderGiver = orderInfo.OrderGiver != null; - msg.Write(hasOrderGiver); - if (hasOrderGiver) { msg.Write(orderInfo.OrderGiver.ID); } - msg.Write((byte)(orderInfo.Option == Identifier.Empty ? 0 : orderInfo.Prefab.Options.IndexOf(orderInfo.Option))); - msg.Write((byte)orderInfo.ManualPriority); + msg.WriteBoolean(hasOrderGiver); + if (hasOrderGiver) { msg.WriteUInt16(orderInfo.OrderGiver.ID); } + msg.WriteByte((byte)(orderInfo.Option == Identifier.Empty ? 0 : orderInfo.Prefab.Options.IndexOf(orderInfo.Option))); + msg.WriteByte((byte)orderInfo.ManualPriority); var hasTargetPosition = orderInfo.TargetPosition != null; - msg.Write(hasTargetPosition); + msg.WriteBoolean(hasTargetPosition); if (hasTargetPosition) { - msg.Write(orderInfo.TargetPosition.Position.X); - msg.Write(orderInfo.TargetPosition.Position.Y); - msg.Write(orderInfo.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.TargetPosition.Hull.ID); + msg.WriteSingle(orderInfo.TargetPosition.Position.X); + msg.WriteSingle(orderInfo.TargetPosition.Position.Y); + msg.WriteUInt16(orderInfo.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.TargetPosition.Hull.ID); } } int ordersLength = msg.LengthBytes - msgLengthBeforeOrders; @@ -715,7 +714,7 @@ namespace Barotrauma WriteStatus(tempBuffer, forceAfflictionData: true); if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize && GameMain.LuaCs.Networking.RestrictMessageSize) { - msg.Write(false); + msg.WriteBoolean(false); if (msgLengthBeforeStatus < 255) { string errorMsg = $"Error when writing character spawn data for \"{Name}\": status data caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})"; @@ -725,7 +724,7 @@ namespace Barotrauma } else { - msg.Write(true); + msg.WriteBoolean(true); WriteStatus(msg, forceAfflictionData: true); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a8723adda..03a39904a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using Barotrauma.Steam; namespace Barotrauma { @@ -300,10 +301,16 @@ namespace Barotrauma Client client = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name, arg)); if (int.TryParse(arg, out int id)) { - client ??= GameMain.Server.ConnectedClients.Find(c => c.ID == id); + client ??= GameMain.Server.ConnectedClients.Find(c => c.SessionId == id); + } + if (Address.Parse(arg).TryUnwrap(out var address)) + { + client ??= GameMain.Server.ConnectedClients.Find(c => c.AddressMatches(address)); + } + if (AccountId.Parse(arg).TryUnwrap(out var argAccountId)) + { + client ??= GameMain.Server.ConnectedClients.Find(c => c.AccountId.ValueEquals(argAccountId)); } - client ??= GameMain.Server.ConnectedClients.Find(c => c.EndpointMatches(arg)); - client ??= GameMain.Server.ConnectedClients.Find(c => c.SteamID == Steam.SteamManager.SteamIDStringToUInt64(arg)); return client; } @@ -873,7 +880,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.Karma, Color.Cyan); + NewMessage("- " + c.SessionId.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Karma, Color.Cyan); } NewMessage("***************", Color.Cyan); }); @@ -882,7 +889,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("***************", client); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Karma, client); + GameMain.Server.SendConsoleMessage("- " + c.SessionId.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Karma, client); } GameMain.Server.SendConsoleMessage("***************", client); }); @@ -905,10 +912,12 @@ namespace Barotrauma client); }); - AssignOnExecute("banendpoint", (string[] args) => + AssignOnExecute("banaddress", (string[] args) => { if (GameMain.Server == null || args.Length == 0) return; + if (!(Address.Parse(args[0]).TryUnwrap(out var address))) { return; } + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"? (c to cancel)", (reason) => { if (reason == "c" || reason == "C") { return; } @@ -926,16 +935,16 @@ namespace Barotrauma banDuration = parsedBanDuration; } - var clients = GameMain.Server.ConnectedClients.FindAll(c => c.EndpointMatches(args[0])); + var clients = GameMain.Server.ConnectedClients.Where(c => c.AddressMatches(address)).ToList(); if (clients.Count == 0) { - GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", args[0], reason, banDuration); + GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", address, reason, banDuration); } else { foreach (Client cl in clients) { - GameMain.Server.BanClient(cl, reason, false, banDuration); + GameMain.Server.BanClient(cl, reason, banDuration); } } }); @@ -1037,7 +1046,8 @@ 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 + $", ping {c.Ping} ms", Color.Cyan); + NewMessage( + $"- {c.SessionId}: {c.Name}{(c.Character != null ? " playing " + c.Character.LogName : "")}, {c.Connection.Endpoint.StringRepresentation}, {c.Connection.AccountInfo.AccountId}, ping {c.Ping} ms", Color.Cyan); } NewMessage("***************", Color.Cyan); })); @@ -1046,7 +1056,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client, Color.Cyan); + GameMain.Server.SendConsoleMessage("- " + c.SessionId.ToString() + ": " + c.Name + ", " + c.Connection.Endpoint.StringRepresentation + $", ping {c.Ping} ms", client, Color.Cyan); } GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); }); @@ -1530,11 +1540,12 @@ namespace Barotrauma ); AssignOnClientRequestExecute( - "banendpoint|banip", + "banaddress|banip", (Client client, Vector2 cursorPos, string[] args) => { - if (args.Length < 1) return; - var clients = GameMain.Server.ConnectedClients.FindAll(c => c.EndpointMatches(args[0])); + if (args.Length < 1) { return; } + if (!(Address.Parse(args[0]).TryUnwrap(out var address))) { return; } + var clients = GameMain.Server.ConnectedClients.Where(c => c.AddressMatches(address)).ToList(); TimeSpan? duration = null; if (args.Length > 1) { @@ -1553,13 +1564,13 @@ namespace Barotrauma if (clients.Count == 0) { - GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", args[0], reason, duration); + GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", address, reason, duration); } else { foreach (Client cl in clients) { - GameMain.Server.BanClient(cl, reason, false, duration); + GameMain.Server.BanClient(cl, reason, duration); } } } @@ -1569,7 +1580,7 @@ namespace Barotrauma { if (GameMain.Server == null || args.Length == 0) return; string clientName = string.Join(" ", args); - GameMain.Server.UnbanPlayer(clientName, ""); + GameMain.Server.UnbanPlayer(clientName); }, () => { @@ -1580,17 +1591,20 @@ namespace Barotrauma }; })); - commands.Add(new Command("unbanip", "unbanip [ip]: Unban a specific IP.", (string[] args) => + commands.Add(new Command("unbanaddress", "unbanaddress [endpoint]: Unban a specific endpoint.", (string[] args) => { if (GameMain.Server == null || args.Length == 0) return; - GameMain.Server.UnbanPlayer("", args[0]); + if (Endpoint.Parse(args[0]).TryUnwrap(out var endpoint)) + { + GameMain.Server.UnbanPlayer(endpoint); + } }, () => { if (GameMain.Server == null) return null; return new string[][] { - GameMain.Server.ServerSettings.BanList.BannedEndPoints.ToArray() + GameMain.Server.ServerSettings.BanList.BannedAddresses.Select(ep => ep.ToString()).ToArray() }; })); @@ -1886,20 +1900,17 @@ namespace Barotrauma } foreach (var talentTree in talentTrees) - { - foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var talentId in talentTree.AllTalentIdentifiers) { - foreach (var option in subTree.TalentOptionStages) + if (TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talentPrefab)) { - foreach (var talent in option.Talents) - { - targetCharacter.GiveTalent(talent); - NewMessage($"Talent \"{talent.DisplayName}\" given to \"{targetCharacter.Name}\" by \"{client.Name}\"."); - GameMain.Server.SendConsoleMessage($"Gave talent \"{talent.DisplayName}\" to \"{targetCharacter.Name}\".", client); - NewMessage($"Unlocked talent \"{talent.DisplayName}\"."); - } + targetCharacter.GiveTalent(talentPrefab); + NewMessage($"Talent \"{talentPrefab.DisplayName}\" given to \"{targetCharacter.Name}\" by \"{client.Name}\"."); + GameMain.Server.SendConsoleMessage($"Gave talent \"{talentPrefab.DisplayName}\" to \"{targetCharacter.Name}\".", client); + NewMessage($"Unlocked talent \"{talentPrefab.DisplayName}\"."); } - } + } } } ); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 718c588b9..c4a043bbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -58,7 +58,7 @@ namespace Barotrauma Reset(); } - private bool IsBlockedByAnotherConversation(IEnumerable targets) + private bool IsBlockedByAnotherConversation(IEnumerable targets, float duration) { foreach (Entity e in targets) { @@ -68,7 +68,7 @@ namespace Barotrauma { if (lastActiveAction.ContainsKey(targetClient) && lastActiveAction[targetClient].ParentEvent != ParentEvent && - Timing.TotalTime < lastActiveAction[targetClient].lastActiveTime + BlockOtherConversationsDuration) + Timing.TotalTime < lastActiveAction[targetClient].lastActiveTime + duration) { return true; } @@ -91,7 +91,7 @@ namespace Barotrauma { targetClients.Add(targetClient); lastActiveAction[targetClient] = this; - ServerWrite(speaker, targetClient); + ServerWrite(speaker, targetClient, interrupt); } } } @@ -105,46 +105,46 @@ namespace Barotrauma { targetClients.Add(c); lastActiveAction[c] = this; - ServerWrite(speaker, c); + ServerWrite(speaker, c, interrupt); } } } } } - private void ServerWrite(Character speaker, Client client) + public void ServerWrite(Character speaker, Client client, bool interrupt) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.EVENTACTION); - outmsg.Write((byte)EventManager.NetworkEventType.CONVERSATION); - outmsg.Write(Identifier); - outmsg.Write(EventSprite); - outmsg.Write((byte)DialogType); - outmsg.Write(ContinueConversation); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION); + outmsg.WriteUInt16(Identifier); + outmsg.WriteString(EventSprite); + outmsg.WriteByte((byte)DialogType); + outmsg.WriteBoolean(ContinueConversation); if (interrupt) { - outmsg.Write(speaker?.ID ?? Entity.NullEntityID); - outmsg.Write(string.Empty); - outmsg.Write(false); - outmsg.Write((byte)0); - outmsg.Write((byte)0); + outmsg.WriteUInt16(speaker?.ID ?? Entity.NullEntityID); + outmsg.WriteString(string.Empty); + outmsg.WriteBoolean(false); + outmsg.WriteByte((byte)0); + outmsg.WriteByte((byte)0); } else { - outmsg.Write(speaker?.ID ?? Entity.NullEntityID); - outmsg.Write(Text ?? string.Empty); - outmsg.Write(FadeToBlack); - outmsg.Write((byte)Options.Count); + outmsg.WriteUInt16(speaker?.ID ?? Entity.NullEntityID); + outmsg.WriteString(Text ?? string.Empty); + outmsg.WriteBoolean(FadeToBlack); + outmsg.WriteByte((byte)Options.Count); for (int i = 0; i < Options.Count; i++) { - outmsg.Write(Options[i].Text); + outmsg.WriteString(Options[i].Text); } int[] endings = GetEndingOptions(); - outmsg.Write((byte)endings.Length); + outmsg.WriteByte((byte)endings.Length); foreach (var end in endings) { - outmsg.Write((byte)end); + outmsg.WriteByte((byte)end); } } GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs index 6eccd6150..a8119efe2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs @@ -10,14 +10,14 @@ namespace Barotrauma private void ServerWrite(IEnumerable targets) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.EVENTACTION); - outmsg.Write((byte)EventManager.NetworkEventType.STATUSEFFECT); - outmsg.Write(ParentEvent.Prefab.Identifier); - outmsg.Write((UInt16)actionIndex); - outmsg.Write((UInt16)targets.Count()); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.STATUSEFFECT); + outmsg.WriteIdentifier(ParentEvent.Prefab.Identifier); + outmsg.WriteUInt16((UInt16)actionIndex); + outmsg.WriteUInt16((UInt16)targets.Count()); foreach (Entity target in targets) { - outmsg.Write(target.ID); + outmsg.WriteUInt16(target.ID); } foreach (Client c in GameMain.Server.ConnectedClients) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 7446dfd5d..dfa4bc4c0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -27,16 +27,24 @@ namespace Barotrauma #endif continue; } - - if (selectedOption == byte.MaxValue) + + if (convAction.SelectedOption > -1) { - convAction.IgnoreClient(sender, 3f); + //someone else already chose an option for this conversation: interrupt for this client + convAction.ServerWrite(convAction.speaker, sender, interrupt: true); } else { - convAction.SelectedOption = selectedOption; + if (selectedOption == byte.MaxValue) + { + convAction.IgnoreClient(sender, 3f); + } + else + { + convAction.SelectedOption = selectedOption; + } } - return; + return; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index 1ab83b7ab..bfa52267f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -12,19 +12,19 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((ushort)spawnedItems.Count); + msg.WriteUInt16((ushort)spawnedItems.Count); foreach (Item item in spawnedItems) { item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } - msg.Write((byte)characters.Count); + msg.WriteByte((byte)characters.Count); foreach (Character character in characters) { character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); - msg.Write(requireKill.Contains(character)); - msg.Write(requireRescue.Contains(character)); - msg.Write((ushort)characterItems[character].Count()); + msg.WriteBoolean(requireKill.Contains(character)); + msg.WriteBoolean(requireRescue.Contains(character)); + msg.WriteUInt16((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs index dd8138d18..af7c2c1a9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs @@ -7,12 +7,12 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((ushort)existingTargets.Count); + msg.WriteUInt16((ushort)existingTargets.Count); foreach (var t in existingTargets) { - msg.Write(t != null ? t.ID : Entity.NullEntityID); + msg.WriteUInt16(t != null ? t.ID : Entity.NullEntityID); } - msg.Write((ushort)spawnedTargets.Count); + msg.WriteUInt16((ushort)spawnedTargets.Count); foreach (var t in spawnedTargets) { t.WriteSpawnData(msg, t.ID, false); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 6b54790d7..3a144fa3b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -7,7 +7,7 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((ushort)items.Count); + msg.WriteUInt16((ushort)items.Count); foreach (Item item in items) { item.WriteSpawnData(msg, diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 5446c04e1..f7656922a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -5,12 +5,16 @@ namespace Barotrauma { partial class CombatMission { + const float RoundEndDuration = 5.0f; + private readonly bool[] teamDead = new bool[2]; private List[] crews; private bool initialized = false; + private float roundEndTimer; + public override LocalizedString Description { get @@ -53,6 +57,7 @@ namespace Barotrauma { teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated); teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated); + if (teamDead[0] && teamDead[1]) { state = 1; } } if (state == 0) @@ -66,13 +71,17 @@ namespace Barotrauma GameMain.GameSession.WinningTeam = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; - state = 1; + //state 1 = team 1 won, 2 = team 2 won + State = i + 1; break; } } } else { + roundEndTimer -= deltaTime; + if (roundEndTimer > 0.0f) { return; } + if (teamDead[0] && teamDead[1]) { GameMain.GameSession.WinningTeam = CharacterTeamType.None; @@ -81,7 +90,7 @@ namespace Barotrauma else if (GameMain.GameSession.WinningTeam != CharacterTeamType.None) { GameMain.Server.EndGame(); - } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs index dea3acc83..5415379fb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -16,12 +16,12 @@ namespace Barotrauma throw new InvalidOperationException("Server attempted to write escort mission data when no characters had been spawned."); } - msg.Write((byte)characters.Count); + msg.WriteByte((byte)characters.Count); foreach (Character character in characters) { character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); - msg.Write(terroristCharacters.Contains(character)); - msg.Write((ushort)characterItems[character].Count()); + msg.WriteBoolean(terroristCharacters.Contains(character)); + msg.WriteUInt16((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index ada2e763a..65a5653e8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -7,17 +7,17 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((byte)caves.Count); + msg.WriteByte((byte)caves.Count); foreach (var cave in caves) { - msg.Write((byte)(Level.Loaded == null || !Level.Loaded.Caves.Contains(cave) ? 255 : Level.Loaded.Caves.IndexOf(cave))); + msg.WriteByte((byte)(Level.Loaded == null || !Level.Loaded.Caves.Contains(cave) ? 255 : Level.Loaded.Caves.IndexOf(cave))); } foreach (var kvp in spawnedResources) { - msg.Write((byte)kvp.Value.Count); + msg.WriteByte((byte)kvp.Value.Count); var rotation = resourceClusters[kvp.Key].Rotation; - msg.Write(rotation); + msg.WriteSingle(rotation); foreach (var r in kvp.Value) { r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); @@ -26,11 +26,11 @@ namespace Barotrauma foreach (var kvp in relevantLevelResources) { - msg.Write(kvp.Key); - msg.Write((byte)kvp.Value.Length); + msg.WriteIdentifier(kvp.Key); + msg.WriteByte((byte)kvp.Value.Length); foreach (var i in kvp.Value) { - msg.Write(i.ID); + msg.WriteUInt16(i.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index 064ccd3e3..433b33075 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -12,18 +12,22 @@ namespace Barotrauma LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; + if (!message.IsNullOrEmpty()) + { + message = ModifyMessage(message, color: false); + } GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage); } public virtual void ServerWriteInitial(IWriteMessage msg, Client c) { - msg.Write((ushort)State); + msg.WriteUInt16((ushort)State); } public virtual void ServerWrite(IWriteMessage msg) { - msg.Write((ushort)State); + msg.WriteUInt16((ushort)State); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs index 937e23511..9af7f048e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs @@ -14,7 +14,7 @@ namespace Barotrauma throw new InvalidOperationException("Server attempted to write monster mission data when no monsters had been spawned."); } - msg.Write((byte)monsters.Count); + msg.WriteByte((byte)monsters.Count); foreach (Character monster in monsters) { monster.WriteSpawnData(msg, monster.ID, restrictMessageSize: false); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index f8e974834..35649469e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -9,10 +9,10 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((byte)(selectedCave == null || Level.Loaded == null || !Level.Loaded.Caves.Contains(selectedCave) ? 255 : Level.Loaded.Caves.IndexOf(selectedCave))); - msg.Write(nestPosition.X); - msg.Write(nestPosition.Y); - msg.Write((ushort)items.Count); + msg.WriteByte((byte)(selectedCave == null || Level.Loaded == null || !Level.Loaded.Caves.Contains(selectedCave) ? 255 : Level.Loaded.Caves.IndexOf(selectedCave))); + msg.WriteSingle(nestPosition.X); + msg.WriteSingle(nestPosition.Y); + msg.WriteUInt16((ushort)items.Count); foreach (Item item in items) { item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs index c04b48601..3295780ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -17,11 +17,11 @@ namespace Barotrauma throw new InvalidOperationException("Server attempted to write escort mission data when no characters had been spawned."); } - msg.Write((byte)characters.Count); + msg.WriteByte((byte)characters.Count); foreach (Character character in characters) { character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); - msg.Write((ushort)characterItems[character].Count()); + msg.WriteUInt16((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index cdc8e3622..52a944a4e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -18,21 +18,21 @@ namespace Barotrauma { base.ServerWriteInitial(msg, c); - msg.Write(usedExistingItem); + msg.WriteBoolean(usedExistingItem); if (usedExistingItem) { - msg.Write(item.ID); + msg.WriteUInt16(item.ID); } else { item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); } - msg.Write((byte)executedEffectIndices.Count); + msg.WriteByte((byte)executedEffectIndices.Count); foreach (Pair effectIndex in executedEffectIndices) { - msg.Write((byte)effectIndex.First); - msg.Write((byte)effectIndex.Second); + msg.WriteByte((byte)effectIndex.First); + msg.WriteByte((byte)effectIndex.Second); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs index 010ed0224..ec766fd9a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -7,7 +7,7 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.Write((ushort)startingItems.Count); + msg.WriteUInt16((ushort)startingItems.Count); foreach (var item in startingItems) { item.WriteSpawnData(msg, @@ -27,11 +27,11 @@ namespace Barotrauma private void ServerWriteScanTargetStatus(IWriteMessage msg) { - msg.Write((byte)scanTargets.Count); + msg.WriteByte((byte)scanTargets.Count); foreach (var kvp in scanTargets) { - msg.Write(kvp.Key != null ? kvp.Key.ID : Entity.NullEntityID); - msg.Write(kvp.Value); + msg.WriteUInt16(kvp.Key != null ? kvp.Key.ID : Entity.NullEntityID); + msg.WriteBoolean(kvp.Value); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 8dbcee776..e931874c2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -157,9 +157,9 @@ namespace Barotrauma string password = ""; bool enableUpnp = false; - int maxPlayers = 10; - int? ownerKey = null; - UInt64 steamId = 0; + int maxPlayers = 10; + Option ownerKey = Option.None(); + Option steamId = Option.None(); IPAddress listenIp = IPAddress.Any; XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); @@ -176,7 +176,7 @@ namespace Barotrauma password = doc.Root.GetAttributeString("password", ""); enableUpnp = doc.Root.GetAttributeBool("enableupnp", false); maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10); - ownerKey = null; + ownerKey = Option.None(); } #if DEBUG @@ -231,12 +231,12 @@ namespace Barotrauma case "-ownerkey": if (int.TryParse(CommandLineArgs[i + 1], out int key)) { - ownerKey = key; + ownerKey = Option.Some(key); } i++; break; case "-steamid": - UInt64.TryParse(CommandLineArgs[i + 1], out steamId); + steamId = SteamId.Parse(CommandLineArgs[i + 1]); i++; break; case "-pipes": @@ -257,6 +257,7 @@ namespace Barotrauma maxPlayers, ownerKey, steamId); + Server.StartServer(); for (int i = 0; i < CommandLineArgs.Length; i++) { @@ -288,7 +289,7 @@ namespace Barotrauma public void CloseServer() { - Server?.Disconnect(); + Server?.Quit(); ShouldRun = false; Server = null; } @@ -345,7 +346,7 @@ namespace Barotrauma if (Server == null) { break; } SteamManager.Update((float)Timing.Step); TaskPool.Update(); - CoroutineManager.Update((float)Timing.Step, (float)Timing.Step); + CoroutineManager.Update(paused: false, (float)Timing.Step); GameMain.LuaCs.Update(); GameMain.LuaCs.Hook.Call("think", new object[] { }); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 2c7358b04..c14233f30 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -47,7 +47,7 @@ namespace Barotrauma public void ServerWriteActiveOrders(IWriteMessage msg) { ushort count = (ushort)ActiveOrders.Count(o => o.Order != null && !o.FadeOutTime.HasValue); - msg.Write(count); + msg.WriteUInt16(count); if (count > 0) { foreach (var activeOrder in ActiveOrders) @@ -55,10 +55,10 @@ namespace Barotrauma if (!(activeOrder?.Order is Order order) || activeOrder.FadeOutTime.HasValue) { continue; } OrderChatMessage.WriteOrder(msg, order, null, isNewOrder: true); bool hasOrderGiver = order.OrderGiver != null; - msg.Write(hasOrderGiver); + msg.WriteBoolean(hasOrderGiver); if (hasOrderGiver) { - msg.Write(order.OrderGiver.ID); + msg.WriteUInt16(order.OrderGiver.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs index 7c0767001..a4f06ea89 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs @@ -6,6 +6,8 @@ namespace Barotrauma { private readonly Queue transactions = new Queue(); + public bool ShouldForceUpdate; + partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { transactions.Enqueue(new WalletChangedData @@ -15,6 +17,15 @@ namespace Barotrauma }); } + /// + /// Forces the server to sync the state of the wallet regardless if the balance/reward has changed + /// + public void ForceUpdate() + { + SettingsChanged(balanceChanged: Option.Some(0), rewardChanged: Option.None()); + ShouldForceUpdate = true; + } + public bool HasTransactions() => transactions.Count > 0; public NetWalletTransaction DequeueAndMergeTransactions(ushort id) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 671dedf34..138dc3cfe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System; using System.Xml.Linq; namespace Barotrauma @@ -15,24 +16,27 @@ namespace Barotrauma public CharacterCampaignData(Client client) { Name = client.Name; - ClientEndPoint = client.Connection.EndPointString; - SteamID = client.SteamID; + ClientAddress = client.Connection.Endpoint.Address; + AccountId = client.AccountId; CharacterInfo = client.CharacterInfo; healthData = new XElement("health"); - client.Character?.CharacterHealth?.Save(healthData); - if (client.Character?.Inventory != null) + + //the character may not be controlled by the client atm, but still exist + Character character = client.Character ?? CharacterInfo?.Character; + + character?.CharacterHealth?.Save(healthData); + if (character?.Inventory != null) { itemData = new XElement("inventory"); - Character.SaveInventory(client.Character.Inventory, itemData); + Character.SaveInventory(character.Inventory, itemData); } OrderData = new XElement("orders"); - if (client.CharacterInfo != null) + if (CharacterInfo != null) { - CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); + CharacterInfo.SaveOrderData(CharacterInfo, OrderData); } - - if (client.Character?.Wallet.Save() is { } walletSave) + if (character?.Wallet.Save() is { } walletSave) { WalletData = walletSave; } @@ -55,13 +59,13 @@ namespace Barotrauma public CharacterCampaignData(XElement element) { Name = element.GetAttributeString("name", "Unnamed"); - ClientEndPoint = element.GetAttributeString("endpoint", null) ?? element.GetAttributeString("ip", ""); - string steamID = element.GetAttributeString("steamid", ""); - if (!string.IsNullOrEmpty(steamID)) - { - ulong.TryParse(steamID, out ulong parsedID); - SteamID = parsedID; - } + string clientEndPointStr = element.GetAttributeString("address", null) + ?? element.GetAttributeString("endpoint", null) + ?? element.GetAttributeString("ip", ""); + ClientAddress = Address.Parse(clientEndPointStr).Fallback(new UnknownAddress()); + string accountIdStr = element.GetAttributeString("accountid", null) + ?? element.GetAttributeString("steamid", ""); + AccountId = Networking.AccountId.Parse(accountIdStr); foreach (XElement subElement in element.Elements()) { @@ -89,19 +93,20 @@ namespace Barotrauma public bool MatchesClient(Client client) { - if (SteamID > 0) + if (AccountId.TryUnwrap(out var accountId) + && client.AccountId.TryUnwrap(out var clientId)) { - return SteamID == client.SteamID; + return accountId == clientId; } else { - return ClientEndPoint == client.Connection.EndPointString; + return ClientAddress == client.Connection.Endpoint.Address; } } public bool IsDuplicate(CharacterCampaignData other) { - return other.SteamID == SteamID && other.ClientEndPoint == ClientEndPoint; + return AccountId == other.AccountId && other.ClientAddress == ClientAddress; } public void SpawnInventoryItems(Character character, Inventory inventory) @@ -117,9 +122,9 @@ namespace Barotrauma character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } - public void ApplyHealthData(Character character) + public void ApplyHealthData(Character character, Func afflictionPredicate = null) { - CharacterInfo.ApplyHealthData(character, healthData); + CharacterInfo.ApplyHealthData(character, healthData, afflictionPredicate); } public void ApplyOrderData(Character character) @@ -136,8 +141,8 @@ namespace Barotrauma { XElement element = new XElement("CharacterCampaignData", new XAttribute("name", Name), - new XAttribute("endpoint", ClientEndPoint), - new XAttribute("steamid", SteamID)); + new XAttribute("address", ClientAddress), + new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : "")); CharacterInfo?.Save(element); if (itemData != null) { element.Add(itemData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6dbe8ff73..502f148fc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { @@ -45,21 +46,26 @@ namespace Barotrauma class SavedExperiencePoints { - public readonly ulong SteamID; - public readonly string EndPoint; + public readonly Option AccountId; + public readonly Address Address; public readonly int ExperiencePoints; public SavedExperiencePoints(Client client) { - SteamID = client.SteamID; - EndPoint = client.Connection.EndPointString; + AccountId = client.AccountId; + Address = client.Connection.Endpoint.Address; ExperiencePoints = client.Character?.Info?.ExperiencePoints ?? 0; } public SavedExperiencePoints(XElement element) { - SteamID = element.GetAttributeUInt64("steamid", 0); - EndPoint = element.GetAttributeString("endpoint", string.Empty); + AccountId = Networking.AccountId.Parse( + element.GetAttributeString("accountid", null) + ?? element.GetAttributeString("steamid", "")); + Address = Address.Parse( + element.GetAttributeString("address", null) + ?? element.GetAttributeString("endpoint", "")) + .Fallback(new UnknownAddress()); ExperiencePoints = element.GetAttributeInt("points", 0); } } @@ -202,11 +208,11 @@ namespace Barotrauma } public int GetSavedExperiencePoints(Client client) { - return savedExperiencePoints.Find(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint))?.ExperiencePoints ?? 0; + return savedExperiencePoints.Find(s => client.AccountId == s.AccountId || client.Connection.Endpoint.Address == s.Address)?.ExperiencePoints ?? 0; } public void ClearSavedExperiencePoints(Client client) { - savedExperiencePoints.RemoveAll(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint)); + savedExperiencePoints.RemoveAll(s => client.AccountId == s.AccountId || client.Connection.Endpoint.Address == s.Address); } public void SavePlayers() @@ -354,6 +360,7 @@ namespace Barotrauma LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + GameMain.GameSession.EventManager.RegisterEventHistory(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -530,8 +537,9 @@ namespace Barotrauma if (wallet.HasTransactions()) { NetWalletTransaction transaction = wallet.DequeueAndMergeTransactions(id); - if (transaction.ChangedData.BalanceChanged.IsNone() && transaction.ChangedData.RewardDistributionChanged.IsNone()) { continue; } + if (!wallet.ShouldForceUpdate && transaction.ChangedData.BalanceChanged.IsNone() && transaction.ChangedData.RewardDistributionChanged.IsNone()) { continue; } transactions.Add(transaction); + wallet.ShouldForceUpdate = false; } } @@ -567,57 +575,57 @@ namespace Barotrauma NetFlags requiredFlags = lastUpdateID.Keys.Where(k => IsFlagRequired(c, k)).Aggregate((NetFlags)0, (f1, f2) => f1 | f2); - msg.Write((UInt16)requiredFlags); + msg.WriteUInt16((UInt16)requiredFlags); - msg.Write(IsFirstRound); - msg.Write(CampaignID); - msg.Write(lastSaveID); - msg.Write(map.Seed); + msg.WriteBoolean(IsFirstRound); + msg.WriteByte(CampaignID); + msg.WriteUInt16(lastSaveID); + msg.WriteString(map.Seed); if (requiredFlags.HasFlag(NetFlags.Misc)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.Misc)); - msg.Write(PurchasedHullRepairs); - msg.Write(PurchasedItemRepairs); - msg.Write(PurchasedLostShuttles); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Misc)); + msg.WriteBoolean(PurchasedHullRepairs); + msg.WriteBoolean(PurchasedItemRepairs); + msg.WriteBoolean(PurchasedLostShuttles); } if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.MapAndMissions)); - msg.Write(ForceMapUI); - msg.Write(map.AllowDebugTeleport); - msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.MapAndMissions)); + msg.WriteBoolean(ForceMapUI); + msg.WriteBoolean(map.AllowDebugTeleport); + msg.WriteUInt16(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); + msg.WriteUInt16(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); if (map.CurrentLocation != null) { - msg.Write((byte)map.CurrentLocation.AvailableMissions.Count()); + msg.WriteByte((byte)map.CurrentLocation.AvailableMissions.Count()); foreach (Mission mission in map.CurrentLocation.AvailableMissions) { - msg.Write(mission.Prefab.Identifier); + msg.WriteIdentifier(mission.Prefab.Identifier); if (mission.Locations[0] == mission.Locations[1]) { - msg.Write((byte)255); + msg.WriteByte((byte)255); } else { Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + msg.WriteByte((byte)map.CurrentLocation.Connections.IndexOf(connection)); } } } else { - msg.Write((byte)0); + msg.WriteByte((byte)0); } var selectedMissionIndices = map.GetSelectedMissionIndices(); - msg.Write((byte)selectedMissionIndices.Count()); + msg.WriteByte((byte)selectedMissionIndices.Count()); foreach (int selectedMissionIndex in selectedMissionIndices) { - msg.Write((byte)selectedMissionIndex); + msg.WriteByte((byte)selectedMissionIndex); } WriteStores(msg); @@ -625,7 +633,7 @@ namespace Barotrauma if (requiredFlags.HasFlag(NetFlags.SubList)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.SubList)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.SubList)); var subList = GameMain.NetLobbyScreen.GetSubList(); List ownedSubmarineIndices = new List(); for (int i = 0; i < subList.Count; i++) @@ -635,83 +643,83 @@ namespace Barotrauma ownedSubmarineIndices.Add(i); } } - msg.Write((ushort)ownedSubmarineIndices.Count); + msg.WriteUInt16((ushort)ownedSubmarineIndices.Count); foreach (int index in ownedSubmarineIndices) { - msg.Write((ushort)index); + msg.WriteUInt16((ushort)index); } } if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.UpgradeManager)); - msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.UpgradeManager)); + msg.WriteUInt16((ushort)UpgradeManager.PendingUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) { - msg.Write(prefab.Identifier); - msg.Write(category.Identifier); - msg.Write((byte)level); + msg.WriteIdentifier(prefab.Identifier); + msg.WriteIdentifier(category.Identifier); + msg.WriteByte((byte)level); } - msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + msg.WriteUInt16((ushort)UpgradeManager.PurchasedItemSwaps.Count); foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { - msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + msg.WriteUInt16(itemSwap.ItemToRemove.ID); + msg.WriteIdentifier(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } } if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate)); WriteItems(msg, CargoManager.ItemsInBuyCrate); WriteStores(msg); } if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate)); WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); WriteStores(msg); } if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.PurchasedItems)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.PurchasedItems)); WriteItems(msg, CargoManager.PurchasedItems); WriteStores(msg); } if (requiredFlags.HasFlag(NetFlags.SoldItems)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.SoldItems)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.SoldItems)); WriteItems(msg, CargoManager.SoldItems); WriteStores(msg); } if (requiredFlags.HasFlag(NetFlags.Reputation)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.Reputation)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation)); Reputation reputation = Map?.CurrentLocation?.Reputation; - msg.Write(reputation != null); - if (reputation != null) { msg.Write(reputation.Value); } + msg.WriteBoolean(reputation != null); + if (reputation != null) { msg.WriteSingle(reputation.Value); } // hopefully we'll never have more than 128 factions - msg.Write((byte)Factions.Count); + msg.WriteByte((byte)Factions.Count); foreach (Faction faction in Factions) { - msg.Write(faction.Prefab.Identifier); - msg.Write(faction.Reputation.Value); + msg.WriteIdentifier(faction.Prefab.Identifier); + msg.WriteSingle(faction.Reputation.Value); } } if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) { - msg.Write(GetLastUpdateIdForFlag(NetFlags.CharacterInfo)); + msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.CharacterInfo)); var characterData = GetClientCharacterData(c); if (characterData?.CharacterInfo == null) { - msg.Write(false); + msg.WriteBoolean(false); } else { - msg.Write(true); + msg.WriteBoolean(true); characterData.CharacterInfo.ServerWrite(msg); } } @@ -722,22 +730,22 @@ namespace Barotrauma { // Store balance bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); - msg.Write(hasStores); + msg.WriteBoolean(hasStores); if (hasStores) { - msg.Write((byte)map.CurrentLocation.Stores.Count); + msg.WriteByte((byte)map.CurrentLocation.Stores.Count); foreach (var store in map.CurrentLocation.Stores.Values) { - msg.Write(store.Identifier); - msg.Write((UInt16)store.Balance); + msg.WriteIdentifier(store.Identifier); + msg.WriteUInt16((UInt16)store.Balance); } } } else { - msg.Write((byte)0); + msg.WriteByte((byte)0); // Store balance - msg.Write(false); + msg.WriteBoolean(false); } } } @@ -790,11 +798,23 @@ namespace Barotrauma purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } + int hullRepairCost = GetHullRepairCost(); + int itemRepairCost = GetItemRepairCost(); + int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; Location location = Map.CurrentLocation; - int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; - int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; - int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; + if (location != null) + { + hullRepairCost = location.GetAdjustedMechanicalCost(hullRepairCost); + itemRepairCost = location.GetAdjustedMechanicalCost(itemRepairCost); + shuttleRetrieveCost = location.GetAdjustedMechanicalCost(shuttleRetrieveCost); + } + Wallet personalWallet = GetWallet(sender); + personalWallet?.ForceUpdate(); + if (AllowedToManageWallets(sender)) + { + Bank.ForceUpdate(); + } if (purchasedHullRepairs != PurchasedHullRepairs) { @@ -875,6 +895,12 @@ namespace Barotrauma { foreach (var item in store.Value.ToList()) { + if (map?.CurrentLocation?.Stores == null || !map.CurrentLocation.Stores.ContainsKey(store.Key)) { continue; } + int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0; + int alreadyPurchasedQuantity = + CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 + + CargoManager.GetPurchasedItem(store.Key, item.ItemPrefab)?.Quantity ?? 0; + item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity); CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); } } @@ -884,13 +910,21 @@ namespace Barotrauma { prevPurchasedItems.Add(kvp.Key, new List(kvp.Value)); } - foreach (var store in prevPurchasedItems) + foreach (var kvp in prevPurchasedItems) { - CargoManager.SellBackPurchasedItems(store.Key, store.Value, sender); + CargoManager.SellBackPurchasedItems(kvp.Key, kvp.Value, sender); } - foreach (var store in purchasedItems) + + foreach (var kvp in purchasedItems) { - CargoManager.PurchaseItems(store.Key, store.Value, false, sender); + var storeId = kvp.Key; + var purchasedItemList = kvp.Value; + foreach (var purchasedItem in purchasedItemList) + { + int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; + purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity); + } + CargoManager.PurchaseItems(storeId, purchasedItemList, false, sender); } foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems) @@ -1239,41 +1273,41 @@ namespace Barotrauma foreach (Client client in GameMain.Server.ConnectedClients) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.CREW); + msg.WriteByte((byte)ServerPacketHeader.CREW); - msg.Write((ushort)availableHires.Count); + msg.WriteUInt16((ushort)availableHires.Count); foreach (CharacterInfo hire in availableHires) { hire.ServerWrite(msg); - msg.Write(hire.Salary); + msg.WriteInt32(hire.Salary); } - msg.Write((ushort)pendingHires.Count); + msg.WriteUInt16((ushort)pendingHires.Count); foreach (CharacterInfo pendingHire in pendingHires) { - msg.Write(pendingHire.GetIdentifierUsingOriginalName()); + msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); } - msg.Write((ushort)(hiredCharacters?.Count ?? 0)); + msg.WriteUInt16((ushort)(hiredCharacters?.Count ?? 0)); if(hiredCharacters != null) { foreach (CharacterInfo info in hiredCharacters) { info.ServerWrite(msg); - msg.Write(info.Salary); + msg.WriteInt32(info.Salary); } } bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName); - msg.Write(validRenaming); + msg.WriteBoolean(validRenaming); if (validRenaming) { - msg.Write(renamedCrewMember.id); - msg.Write(renamedCrewMember.newName); + msg.WriteInt32(renamedCrewMember.id); + msg.WriteString(renamedCrewMember.newName); } - msg.Write(firedCharacter != null); - if (firedCharacter != null) { msg.Write(firedCharacter.GetIdentifier()); } + msg.WriteBoolean(firedCharacter != null); + if (firedCharacter != null) { msg.WriteInt32(firedCharacter.GetIdentifier()); } GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -1351,8 +1385,8 @@ namespace Barotrauma foreach (var savedExperiencePoint in savedExperiencePoints) { savedExperiencePointsElement.Add(new XElement("Point", - new XAttribute("steamid", savedExperiencePoint.SteamID), - new XAttribute("endpoint", savedExperiencePoint?.EndPoint ?? string.Empty), + new XAttribute("accountid", savedExperiencePoint.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : ""), + new XAttribute("address", savedExperiencePoint.Address.StringRepresentation), new XAttribute("points", savedExperiencePoint.ExperiencePoints))); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e9c23978c..e30b1148f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -154,7 +154,7 @@ namespace Barotrauma private IWriteMessage StartSending() { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.MEDICAL); + msg.WriteByte((byte)ServerPacketHeader.MEDICAL); return msg; } @@ -181,8 +181,8 @@ namespace Barotrauma } IWriteMessage msg = StartSending(); - msg.Write((byte)header); - msg.Write((byte)flag); + msg.WriteByte((byte)header); + msg.WriteByte((byte)flag); netStruct?.Write(msg); GameMain.Server.ServerPeer.Send(msg, c.Connection, deliveryMethod); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs index 3ac8dd6a9..13411d051 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -17,26 +17,26 @@ namespace Barotrauma if (client != null && !client.Spectating) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ServerPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.Start); - msg.Write(new DateTimeOffset(startTime).ToUnixTimeSeconds()); - msg.Write(new DateTimeOffset(endTime).ToUnixTimeSeconds()); - msg.Write(author); + msg.WriteByte((byte)ServerPacketHeader.READY_CHECK); + msg.WriteByte((byte)ReadyCheckState.Start); + msg.WriteInt64(new DateTimeOffset(startTime).ToUnixTimeSeconds()); + msg.WriteInt64(new DateTimeOffset(endTime).ToUnixTimeSeconds()); + msg.WriteString(author); if (sender != null) { - msg.Write(true); - msg.Write(sender.ID); + msg.WriteBoolean(true); + msg.WriteByte(sender.SessionId); } else { - msg.Write(false); + msg.WriteBoolean(false); } - msg.Write((ushort) ActivePlayers.Count); + msg.WriteUInt16((ushort)ActivePlayers.Count); foreach (byte clientId in Clients.Keys) { - msg.Write(clientId); + msg.WriteByte(clientId); } GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); @@ -55,10 +55,10 @@ namespace Barotrauma foreach (Client client in ActivePlayers) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.READY_CHECK); - msg.Write((byte)ReadyCheckState.Update); - msg.Write((byte)state); - msg.Write(otherClient); + msg.WriteByte((byte)ServerPacketHeader.READY_CHECK); + msg.WriteByte((byte)ReadyCheckState.Update); + msg.WriteByte((byte)state); + msg.WriteByte(otherClient); GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } } @@ -72,13 +72,13 @@ namespace Barotrauma if (client != null && !client.Spectating) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ServerPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.End); - msg.Write((ushort) Clients.Count); + msg.WriteByte((byte)ServerPacketHeader.READY_CHECK); + msg.WriteByte((byte)ReadyCheckState.End); + msg.WriteUInt16((ushort)Clients.Count); foreach (var (id, state) in Clients) { - msg.Write(id); - msg.Write((byte) state); + msg.WriteByte(id); + msg.WriteByte((byte)state); } GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); @@ -88,7 +88,7 @@ namespace Barotrauma public static void ServerRead(IReadMessage inc, Client client) { - ReadyCheckState state = (ReadyCheckState) inc.ReadByte(); + ReadyCheckState state = (ReadyCheckState)inc.ReadByte(); ReadyCheck? readyCheck = GameMain.GameSession?.CrewManager?.ActiveReadyCheck; switch (state) @@ -98,11 +98,11 @@ namespace Barotrauma break; case ReadyCheckState.Update when readyCheck != null: - ReadyStatus status = (ReadyStatus) inc.ReadByte(); - if (!readyCheck.Clients.ContainsKey(client.ID)) { return; } + ReadyStatus status = (ReadyStatus)inc.ReadByte(); + if (!readyCheck.Clients.ContainsKey(client.SessionId)) { return; } - readyCheck.Clients[client.ID] = status; - readyCheck.UpdateReadyCheck(client.ID, status); + readyCheck.Clients[client.SessionId] = status; + readyCheck.UpdateReadyCheck(client.SessionId, status); break; } } @@ -111,8 +111,8 @@ namespace Barotrauma { if (GameMain.GameSession?.CrewManager == null || GameMain.GameSession.CrewManager.ActiveReadyCheck != null) { return; } - List connectedClients = GameMain.Server.ConnectedClients; - ReadyCheck newReadyCheck = new ReadyCheck(connectedClients.Where(c => !c.Spectating).Select(c => c.ID).ToList(), 30); + var connectedClients = GameMain.Server.ConnectedClients; + ReadyCheck newReadyCheck = new ReadyCheck(connectedClients.Where(c => !c.Spectating).Select(c => c.SessionId).ToList(), 30); GameMain.GameSession.CrewManager.ActiveReadyCheck = newReadyCheck; newReadyCheck.InitializeReadyCheck(author, sender); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index 607149e26..b5349e891 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -6,11 +6,11 @@ namespace Barotrauma.Items.Components { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(docked); + msg.WriteBoolean(docked); if (docked) { - msg.Write(DockingTarget.item.ID); - msg.Write(IsLocked); + msg.WriteUInt16(DockingTarget.item.ID); + msg.WriteBoolean(IsLocked); } } public void ServerEventRead(IReadMessage msg, Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 3edb8ff4e..6f980eab7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -38,13 +38,13 @@ namespace Barotrauma.Items.Components bool forcedOpen = TryExtractEventData(extraData, out var eventData) && eventData.ForcedOpen; base.ServerEventWrite(msg, c, extraData); - msg.Write(isOpen); - msg.Write(isBroken); - msg.Write(forcedOpen); //forced open - msg.Write(isStuck); - msg.Write(isJammed); + msg.WriteBoolean(isOpen); + msg.WriteBoolean(isBroken); + msg.WriteBoolean(forcedOpen); //forced open + msg.WriteBoolean(isStuck); + msg.WriteBoolean(isJammed); msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); - msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID); + msg.WriteUInt16(lastUser == null ? (UInt16)0 : lastUser.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs index ff82cc9d8..6951f65c5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -6,14 +6,14 @@ namespace Barotrauma.Items.Components { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(tainted); + msg.WriteBoolean(tainted); if (tainted) { - msg.Write(selectedTaintedEffect?.UintIdentifier ?? 0); + msg.WriteUInt32(selectedTaintedEffect?.UintIdentifier ?? 0); } else { - msg.Write(selectedEffect?.UintIdentifier ?? 0); + msg.WriteUInt32(selectedEffect?.UintIdentifier ?? 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index 8fd930fc8..8749382f1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.WriteRangedSingle(Health, 0f, (float) MaxHealth, 8); + msg.WriteRangedSingle(Health, 0f, (float)MaxHealth, 8); if (TryExtractEventData(extraData, out EventData eventData)) { int offset = eventData.Offset; @@ -48,11 +48,11 @@ namespace Barotrauma.Items.Components { VineTile vine = Vines[i]; var (x, y) = vine.Position; - msg.WriteRangedInteger((byte) vine.Type, 0b0000, 0b1111); + msg.WriteRangedInteger((byte)vine.Type, 0b0000, 0b1111); msg.WriteRangedInteger(vine.FlowerConfig.Serialize(), 0, 0xFFF); msg.WriteRangedInteger(vine.LeafConfig.Serialize(), 0, 0xFFF); - msg.Write((byte) (x / VineTile.Size)); - msg.Write((byte) (y / VineTile.Size)); + msg.WriteByte((byte)(x / VineTile.Size)); + msg.WriteByte((byte)(y / VineTile.Size)); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 0b8521700..98c6a0fa9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -10,13 +10,13 @@ namespace Barotrauma.Items.Components base.ServerEventWrite(msg, c, extraData); bool writeAttachData = attachable && body != null; - msg.Write(writeAttachData); + msg.WriteBoolean(writeAttachData); if (!writeAttachData) { return; } - msg.Write(Attached); - msg.Write(body.SimPosition.X); - msg.Write(body.SimPosition.Y); - msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); + msg.WriteBoolean(Attached); + msg.WriteSingle(body.SimPosition.X); + msg.WriteSingle(body.SimPosition.Y); + msg.WriteUInt16(item.Submarine?.ID ?? Entity.NullEntityID); } public void ServerEventRead(IReadMessage msg, Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs index 3d372c206..bd9a54c1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(deattachTimer); + msg.WriteSingle(deattachTimer); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index ad7dd525f..dbcf85e4d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(Text); + msg.WriteString(Text); lastSentText = Text; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs index 4396628d9..bf7e8e2c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(IsActive); + msg.WriteBoolean(IsActive); lastSentState = IsActive; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index dc87c0b4a..8d8fe2933 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -6,8 +6,8 @@ namespace Barotrauma.Items.Components { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(State); - msg.Write(user == null ? (ushort)0 : user.ID); + msg.WriteBoolean(State); + msg.WriteUInt16(user == null ? (ushort)0 : user.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs index 9897bc414..91b7515b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs @@ -18,9 +18,9 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(user?.ID ?? 0); - msg.Write(IsActive); - msg.Write(progressTimer); + msg.WriteUInt16(user?.ID ?? 0); + msg.WriteBoolean(IsActive); + msg.WriteSingle(progressTimer); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index a1f431279..b611a9b7f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { //force can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); - msg.Write(User == null ? Entity.NullEntityID : User.ID); + msg.WriteUInt16(User == null ? Entity.NullEntityID : User.ID); } public void ServerEventRead(IReadMessage msg, Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 347ef4ed5..c3a383431 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -55,18 +55,18 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { var componentData = ExtractEventData(extraData); - msg.Write((byte)componentData.State); - msg.Write(timeUntilReady); + msg.WriteByte((byte)componentData.State); + msg.WriteSingle(timeUntilReady); uint recipeHash = fabricatedItem?.RecipeHash ?? 0; - msg.Write(recipeHash); - UInt16 userID = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; - msg.Write(userID); + msg.WriteUInt32(recipeHash); + UInt16 userId = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; + msg.WriteUInt16(userId); var reachedLimits = fabricationLimits.Where(kvp => kvp.Value <= 0); - msg.Write((ushort)reachedLimits.Count()); + msg.WriteUInt16((ushort)reachedLimits.Count()); foreach (var kvp in reachedLimits) { - msg.Write(kvp.Key); + msg.WriteUInt32(kvp.Key); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 125557284..a026feea0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -49,16 +49,16 @@ namespace Barotrauma.Items.Components { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); - msg.Write(IsActive); - msg.Write(Hijacked); + msg.WriteBoolean(IsActive); + msg.WriteBoolean(Hijacked); if (TargetLevel != null) { - msg.Write(true); - msg.Write(TargetLevel.Value); + msg.WriteBoolean(true); + msg.WriteSingle(TargetLevel.Value); } else { - msg.Write(false); + msg.WriteBoolean(false); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index 91f9f02ad..0cfbe9026 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -45,8 +45,8 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(autoTemp); - msg.Write(_powerOn); + msg.WriteBoolean(autoTemp); + msg.WriteBoolean(_powerOn); msg.WriteRangedSingle(temperature, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 4145c0b35..f618c0a72 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -100,30 +100,30 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Barotrauma.Networking.Client c, NetEntityEvent.IData extraData = null) { - msg.Write(autoPilot); - msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); - msg.Write(user?.ID ?? Entity.NullEntityID); + msg.WriteBoolean(autoPilot); + msg.WriteBoolean(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); + msg.WriteUInt16(user?.ID ?? Entity.NullEntityID); if (!autoPilot) { //no need to write steering info if autopilot is controlling - msg.Write(steeringInput.X); - msg.Write(steeringInput.Y); - msg.Write(targetVelocity.X); - msg.Write(targetVelocity.Y); - msg.Write(steeringAdjustSpeed); + msg.WriteSingle(steeringInput.X); + msg.WriteSingle(steeringInput.Y); + msg.WriteSingle(targetVelocity.X); + msg.WriteSingle(targetVelocity.Y); + msg.WriteSingle(steeringAdjustSpeed); } else { - msg.Write(posToMaintain != null); + msg.WriteBoolean(posToMaintain != null); if (posToMaintain != null) { - msg.Write(((Vector2)posToMaintain).X); - msg.Write(((Vector2)posToMaintain).Y); + msg.WriteSingle(((Vector2)posToMaintain).X); + msg.WriteSingle(((Vector2)posToMaintain).Y); } else { - msg.Write(LevelStartSelected); + msg.WriteBoolean(LevelStartSelected); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 7bf233c95..60cc5c609 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -25,39 +25,39 @@ namespace Barotrauma.Items.Components var eventData = ExtractEventData(extraData); bool launch = eventData.Launch; - msg.Write(launch); + msg.WriteBoolean(launch); if (launch) { - msg.Write(User.ID); - msg.Write(launchPos.X); - msg.Write(launchPos.Y); - msg.Write(launchRot); + msg.WriteUInt16(User.ID); + msg.WriteSingle(launchPos.X); + msg.WriteSingle(launchPos.Y); + msg.WriteSingle(launchRot); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); - msg.Write(stuck); + msg.WriteBoolean(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(jointAxis.X); - msg.Write(jointAxis.Y); + msg.WriteUInt16(item.Submarine?.ID ?? Entity.NullEntityID); + msg.WriteUInt16(item.CurrentHull?.ID ?? Entity.NullEntityID); + msg.WriteSingle(item.SimPosition.X); + msg.WriteSingle(item.SimPosition.Y); + msg.WriteSingle(jointAxis.X); + msg.WriteSingle(jointAxis.Y); if (StickTarget.UserData is Structure structure) { - msg.Write(structure.ID); + msg.WriteUInt16(structure.ID); int bodyIndex = structure.Bodies.IndexOf(StickTarget); - msg.Write((byte)(bodyIndex == -1 ? 0 : bodyIndex)); + msg.WriteByte((byte)(bodyIndex == -1 ? 0 : bodyIndex)); } else if (StickTarget.UserData is Entity entity) { - msg.Write(entity.ID); + msg.WriteUInt16(entity.ID); } else if (StickTarget.UserData is Limb limb) { - msg.Write(limb.character.ID); - msg.Write((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); + msg.WriteUInt16(limb.character.ID); + msg.WriteByte((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 41d2f5c1d..e4e5845c9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -44,13 +44,13 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(deteriorationTimer); - msg.Write(deteriorateAlwaysResetTimer); - msg.Write(DeteriorateAlways); - msg.Write(tinkeringDuration); - msg.Write(tinkeringStrength); - msg.Write(tinkeringPowersDevices); - msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); + msg.WriteSingle(deteriorationTimer); + msg.WriteSingle(deteriorateAlwaysResetTimer); + msg.WriteBoolean(DeteriorateAlways); + msg.WriteSingle(tinkeringDuration); + msg.WriteSingle(tinkeringStrength); + msg.WriteBoolean(tinkeringPowersDevices); + msg.WriteUInt16(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index 81b46fe2a..11342ebed 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -6,25 +6,25 @@ namespace Barotrauma.Items.Components { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(Snapped); + msg.WriteBoolean(Snapped); if (!Snapped) { - msg.Write(target?.ID ?? Entity.NullEntityID); + msg.WriteUInt16(target?.ID ?? Entity.NullEntityID); if (source is Entity entity && !entity.Removed) { - msg.Write(entity?.ID ?? Entity.NullEntityID); - msg.Write((byte)0); + msg.WriteUInt16(entity?.ID ?? Entity.NullEntityID); + msg.WriteByte((byte)0); } else if (source is Limb limb && limb.character != null && !limb.character.Removed) { - msg.Write(limb.character?.ID ?? Entity.NullEntityID); - msg.Write((byte)limb.character.AnimController.Limbs.IndexOf(limb)); + msg.WriteUInt16(limb.character?.ID ?? Entity.NullEntityID); + msg.WriteByte((byte)limb.character.AnimController.Limbs.IndexOf(limb)); } else { - msg.Write(Entity.NullEntityID); - msg.Write((byte)0); + msg.WriteUInt16(Entity.NullEntityID); + msg.WriteByte((byte)0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs index 2af1acfea..3b1b85227 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(scanTimer); + msg.WriteSingle(scanTimer); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index a9cffe9ef..7ba476df9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -208,7 +208,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(user == null ? (ushort)0 : user.ID); + msg.WriteUInt16(user == null ? (ushort)0 : user.ID); ClientEventWrite(msg, extraData); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index bf2df30bd..7d5e8ea6a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -72,15 +72,15 @@ namespace Barotrauma.Items.Components var element = customInterfaceElementList[i]; if (element.HasPropertyName) { - msg.Write(element.Signal); + msg.WriteString(element.Signal); } else if(element.ContinuousSignal) { - msg.Write(element.State); + msg.WriteBoolean(element.State); } else { - msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs index d99732838..189f07c27 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(Value); + msg.WriteString(Value); lastSentValue = Value; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 9fdd21863..8beb42942 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -101,11 +101,11 @@ namespace Barotrauma.Items.Components { if (TryExtractEventData(extraData, out ServerEventData eventData)) { - msg.Write(eventData.MsgToSend); + msg.WriteString(eventData.MsgToSend); } else { - msg.Write(OutputValue); + msg.WriteString(OutputValue); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs index 51a0fa393..59adff519 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs @@ -41,8 +41,8 @@ namespace Barotrauma.Items.Components msg.WriteRangedInteger(nodeCount, 0, MaxNodesPerNetworkEvent); for (int i = nodeStartIndex; i < nodeStartIndex + nodeCount; i++) { - msg.Write(nodes[i].X); - msg.Write(nodes[i].Y); + msg.WriteSingle(nodes[i].X); + msg.WriteSingle(nodes[i].Y); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 32f261856..862478ce6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -62,14 +62,14 @@ namespace Barotrauma throw error("component \"" + components[containerIndex] + "\" is not server serializable"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); - msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); + msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; case ItemStatusEventData _: - msg.Write(condition); + msg.WriteSingle(condition); break; case AssignCampaignInteractionEventData _: - msg.Write((byte)CampaignInteractionType); + msg.WriteByte((byte)CampaignInteractionType); break; case ApplyStatusEffectEventData applyStatusEffectEventData: { @@ -83,15 +83,15 @@ namespace Barotrauma byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); - msg.Write((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent))); - msg.Write(applyStatusEffectEventData.TargetCharacter?.ID ?? (ushort)0); - msg.Write(targetLimbIndex); - msg.Write(applyStatusEffectEventData.UseTarget?.ID ?? (ushort)0); - msg.Write(worldPosition.HasValue); + msg.WriteByte((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent))); + msg.WriteUInt16(applyStatusEffectEventData.TargetCharacter?.ID ?? (ushort)0); + msg.WriteByte(targetLimbIndex); + msg.WriteUInt16(applyStatusEffectEventData.UseTarget?.ID ?? (ushort)0); + msg.WriteBoolean(worldPosition.HasValue); if (worldPosition.HasValue) { - msg.Write(worldPosition.Value.X); - msg.Write(worldPosition.Value.Y); + msg.WriteSingle(worldPosition.Value.X); + msg.WriteSingle(worldPosition.Value.Y); } } break; @@ -109,16 +109,16 @@ namespace Barotrauma case UpgradeEventData upgradeEventData: var upgrade = upgradeEventData.Upgrade; var upgradeTargets = upgrade.TargetComponents; - msg.Write(upgrade.Identifier); - msg.Write((byte)upgrade.Level); - msg.Write((byte)upgradeTargets.Count); + msg.WriteIdentifier(upgrade.Identifier); + msg.WriteByte((byte)upgrade.Level); + msg.WriteByte((byte)upgradeTargets.Count); foreach (var (_, value) in upgrade.TargetComponents) { - msg.Write((byte)value.Length); + msg.WriteByte((byte)value.Length); foreach (var propertyReference in value) { object originalValue = propertyReference.OriginalValue; - msg.Write((float)(originalValue ?? -1)); + msg.WriteSingle((float)(originalValue ?? -1)); } } break; @@ -189,35 +189,35 @@ namespace Barotrauma { if (GameMain.Server == null) { return; } - msg.Write(Prefab.OriginalName); - msg.Write(Prefab.Identifier); - msg.Write(Description != base.Prefab.Description); + msg.WriteString(Prefab.OriginalName); + msg.WriteIdentifier(Prefab.Identifier); + msg.WriteBoolean(Description != base.Prefab.Description); if (Description != base.Prefab.Description) { - msg.Write(Description); + msg.WriteString(Description); } - msg.Write(entityID); + msg.WriteUInt16(entityID); if (ParentInventory == null || ParentInventory.Owner == null || originalInventoryID == 0) { - msg.Write((ushort)0); + msg.WriteUInt16((ushort)0); - msg.Write(Position.X); - msg.Write(Position.Y); + msg.WriteSingle(Position.X); + msg.WriteSingle(Position.Y); msg.WriteRangedSingle(body == null ? 0.0f : MathUtils.WrapAngleTwoPi(body.Rotation), 0.0f, MathHelper.TwoPi, 8); - msg.Write(Submarine != null ? Submarine.ID : (ushort)0); + msg.WriteUInt16(Submarine != null ? Submarine.ID : (ushort)0); } else { - msg.Write(originalInventoryID); - msg.Write(originalItemContainerIndex); - msg.Write(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex); + msg.WriteUInt16(originalInventoryID); + msg.WriteByte(originalItemContainerIndex); + msg.WriteByte(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex); } - msg.Write(body == null ? (byte)0 : (byte)body.BodyType); - msg.Write(SpawnedInCurrentOutpost); - msg.Write(AllowStealing); + msg.WriteByte(body == null ? (byte)0 : (byte)body.BodyType); + msg.WriteBoolean(SpawnedInCurrentOutpost); + msg.WriteBoolean(AllowStealing); msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality); byte teamID = 0; @@ -237,39 +237,39 @@ namespace Barotrauma } } - msg.Write(teamID); + msg.WriteByte(teamID); bool hasIdCard = idCardComponent != null; - msg.Write(hasIdCard); + msg.WriteBoolean(hasIdCard); if (hasIdCard) { - msg.Write(idCardComponent.OwnerName); - msg.Write(idCardComponent.OwnerTags); - msg.Write((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); - msg.Write((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); - msg.Write((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); - msg.Write((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); + msg.WriteString(idCardComponent.OwnerName); + msg.WriteString(idCardComponent.OwnerTags); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor); - msg.Write(idCardComponent.OwnerJobId); - msg.Write((byte)idCardComponent.OwnerSheetIndex.X); - msg.Write((byte)idCardComponent.OwnerSheetIndex.Y); + msg.WriteIdentifier(idCardComponent.OwnerJobId); + msg.WriteByte((byte)idCardComponent.OwnerSheetIndex.X); + msg.WriteByte((byte)idCardComponent.OwnerSheetIndex.Y); } bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t)); - msg.Write(tagsChanged); + msg.WriteBoolean(tagsChanged); if (tagsChanged) { IEnumerable splitTags = Tags.Split(',').ToIdentifiers(); - msg.Write(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t)))); - msg.Write(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t)))); + msg.WriteString(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t)))); + msg.WriteString(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t)))); } var nameTag = GetComponent(); - msg.Write(nameTag != null); + msg.WriteBoolean(nameTag != null); if (nameTag != null) { - msg.Write(nameTag.WrittenName ?? ""); + msg.WriteString(nameTag.WrittenName ?? ""); } } @@ -342,12 +342,12 @@ namespace Barotrauma public void ServerWritePosition(IWriteMessage msg, Client c) { - msg.Write(ID); + msg.WriteUInt16(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); body.ServerWrite(tempBuffer); msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs index cdbd0c952..f9bf125de 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs @@ -32,21 +32,21 @@ namespace Barotrauma { if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed level event: expected {nameof(Level)}.{nameof(IEventData)}"); } - msg.Write((byte)eventData.EventType); + msg.WriteByte((byte)eventData.EventType); switch (eventData) { case SingleLevelWallEventData { Wall: var destructibleWall }: int index = ExtraWalls.IndexOf(destructibleWall); - msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); + msg.WriteUInt16((ushort)(index == -1 ? ushort.MaxValue : index)); //write health using one byte - msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); + msg.WriteByte((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); break; case GlobalLevelWallEventData _: foreach (LevelWall levelWall in ExtraWalls) { if (levelWall.Body.BodyType == BodyType.Static) { continue; } - msg.Write(levelWall.Body.Position.X); - msg.Write(levelWall.Body.Position.Y); + msg.WriteSingle(levelWall.Body.Position.X); + msg.WriteSingle(levelWall.Body.Position.Y); msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 877d0b4b1..671413642 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -56,7 +56,7 @@ namespace Barotrauma.MapCreatures.Behavior public void ServerWrite(IWriteMessage msg, IEventData eventData) { - msg.Write((byte)eventData.NetworkHeader); + msg.WriteByte((byte)eventData.NetworkHeader); switch (eventData) { @@ -80,51 +80,51 @@ namespace Barotrauma.MapCreatures.Behavior break; } - msg.Write(PowerConsumptionTimer); + msg.WriteSingle(PowerConsumptionTimer); } private void ServerWriteSpawn(IWriteMessage msg) { - msg.Write(Prefab.Identifier); - msg.Write(Offset.X); - msg.Write(Offset.Y); + msg.WriteIdentifier(Prefab.Identifier); + msg.WriteSingle(Offset.X); + msg.WriteSingle(Offset.Y); } private void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) { var (x, y) = branch.Position; - msg.Write(parentId); - msg.Write((int)branch.ID); - msg.Write(branch.IsRootGrowth); + msg.WriteInt32(parentId); + msg.WriteInt32((int)branch.ID); + msg.WriteBoolean(branch.IsRootGrowth); msg.WriteRangedInteger((byte)branch.Type, 0b0000, 0b1111); msg.WriteRangedInteger((byte)branch.Sides, 0b0000, 0b1111); msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); msg.WriteRangedInteger(branch.LeafConfig.Serialize(), 0, 0xFFF); - msg.Write((ushort)branch.MaxHealth); - msg.Write((int)(x / VineTile.Size)); - msg.Write((int)(y / VineTile.Size)); - msg.Write(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); + msg.WriteUInt16((ushort)branch.MaxHealth); + msg.WriteInt32((int)(x / VineTile.Size)); + msg.WriteInt32((int)(y / VineTile.Size)); + msg.WriteInt32(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); } private void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) { - msg.Write((int)branch.ID); - msg.Write(branch.Health); + msg.WriteInt32((int)branch.ID); + msg.WriteSingle(branch.Health); } private void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, InfectEventData.InfectState infect, BallastFloraBranch infector = null) { - msg.Write(itemID); - msg.Write(infect == InfectEventData.InfectState.Yes); + msg.WriteUInt16(itemID); + msg.WriteBoolean(infect == InfectEventData.InfectState.Yes); if (infect == InfectEventData.InfectState.Yes) { - msg.Write(infector?.ID ?? -1); + msg.WriteInt32(infector?.ID ?? -1); } } private void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) { - msg.Write(branch.ID); + msg.WriteInt32(branch.ID); } public void CreateNetworkMessage(IEventData extraData) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 9efc0c125..74ca52bb7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -94,8 +94,8 @@ namespace Barotrauma msg.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); foreach (Decal decal in decals) { - msg.Write(decal.Prefab.UintIdentifier); - msg.Write((byte)decal.SpriteIndex); + msg.WriteUInt32(decal.Prefab.UintIdentifier); + msg.WriteByte((byte)decal.SpriteIndex); float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.Y), 0.0f, 1.0f); msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs index 1f6f58360..7a5a03496 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs @@ -11,7 +11,7 @@ namespace Barotrauma public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write((byte)Sections.Length); + msg.WriteByte((byte)Sections.Length); for (int i = 0; i < Sections.Length; i++) { msg.WriteRangedSingle(Sections[i].damage / MaxHealth, 0.0f, 1.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 9f634bd1d..572ffaab8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -7,11 +7,11 @@ namespace Barotrauma { public void ServerWritePosition(IWriteMessage msg, Client c) { - msg.Write(ID); + msg.WriteUInt16(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); subBody.Body.ServerWrite(tempBuffer); - msg.Write((byte)tempBuffer.LengthBytes); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + msg.WriteByte((byte)tempBuffer.LengthBytes); + msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index c324baf78..15f4d365a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -1,93 +1,68 @@ -using System; -using System.Collections.Generic; +#nullable enable +using System; using Barotrauma.IO; using System.Linq; -using System.Net; -using Barotrauma.Steam; +using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Networking { partial class BannedPlayer { - private static UInt16 LastIdentifier = 0; + private static UInt32 LastIdentifier = 0; - public BannedPlayer(string name, string endPoint, string reason, DateTime? expirationTime) + public bool Expired => ExpirationTime is { } expirationTime && DateTime.Now > expirationTime; + + public BannedPlayer( + string name, Either addressOrAccountId, string reason, DateTime? expirationTime) { this.Name = name; - this.EndPoint = endPoint; - ParseEndPointAsSteamId(); + this.AddressOrAccountId = addressOrAccountId; this.Reason = reason; this.ExpirationTime = expirationTime; this.UniqueIdentifier = LastIdentifier; LastIdentifier++; - - this.IsRangeBan = EndPoint.IndexOf(".x") > -1; - } - - public BannedPlayer(string name, ulong steamID, string reason, DateTime? expirationTime) - { - this.Name = name; - this.SteamID = steamID; - this.Reason = reason; - this.ExpirationTime = expirationTime; - this.UniqueIdentifier = LastIdentifier; LastIdentifier++; - - this.IsRangeBan = false; - - this.EndPoint = ""; - } - - public bool CompareTo(string endpointCompare) - { - if (string.IsNullOrEmpty(EndPoint) || string.IsNullOrEmpty(endpointCompare)) { return false; } - if (!IsRangeBan) - { - return endpointCompare == EndPoint; - } - else - { - int rangeBanIndex = EndPoint.IndexOf(".x"); - if (endpointCompare.Length < rangeBanIndex) return false; - return endpointCompare.Substring(0, rangeBanIndex) == EndPoint.Substring(0, rangeBanIndex); - } - } - - public bool CompareTo(IPAddress ipCompare) - { - if (string.IsNullOrEmpty(EndPoint) || ipCompare == null) { return false; } - if (ipCompare.IsIPv4MappedToIPv6 && CompareTo(ipCompare.MapToIPv4NoThrow().ToString())) - { - return true; - } - return CompareTo(ipCompare.ToString()); } } partial class BanList { - const string SavePath = "Data/bannedplayers.txt"; + private const string SavePath = "Data/bannedplayers.xml"; + private const string LegacySavePath = "Data/bannedplayers.txt"; partial void InitProjectSpecific() { - if (!File.Exists(SavePath)) { return; } + if (!File.Exists(SavePath)) + { + LoadLegacyBanList(); + } + else + { + LoadBanList(); + } + } + private void LoadLegacyBanList() + { + if (!File.Exists(LegacySavePath)) { return; } + string[] lines; try { - lines = File.ReadAllLines(SavePath); + lines = File.ReadAllLines(LegacySavePath); } catch (Exception e) { - DebugConsole.ThrowError("Failed to open the list of banned players in " + SavePath, e); + DebugConsole.ThrowError($"Failed to open the list of banned players in {LegacySavePath}", e); return; } foreach (string line in lines) { string[] separatedLine = line.Split(','); - if (separatedLine.Length < 2) continue; + if (separatedLine.Length < 2) { continue; } string name = separatedLine[0]; - string identifier = separatedLine[1]; + string endpointStr = separatedLine[1]; DateTime? expirationTime = null; if (separatedLine.Length > 2 && !string.IsNullOrEmpty(separatedLine[2])) @@ -96,99 +71,105 @@ namespace Barotrauma.Networking { expirationTime = parsedTime; } + else + { + string error = $"Failed to parse the ban duration of \"{name}\" ({separatedLine[2]}) from the legacy ban list file (text file which has now been changed to XML). Considering the ban permanent."; + DebugConsole.ThrowError(error); + GameServer.AddPendingMessageToOwner(error, ChatMessageType.Error); + } } string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : ""; - if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) continue; + if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) { continue; } - if (identifier.Contains(".") || identifier.Contains(":")) + if (AccountId.Parse(endpointStr).TryUnwrap(out var accountId)) { - //identifier is an ip - bannedPlayers.Add(new BannedPlayer(name, identifier, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, accountId, reason, expirationTime)); } - else + else if (Address.Parse(endpointStr).TryUnwrap(out var address)) { - //identifier should be a steam id - if (ulong.TryParse(identifier, out ulong steamID)) - { - bannedPlayers.Add(new BannedPlayer(name, steamID, reason, expirationTime)); - } - else - { - DebugConsole.ThrowError("Error in banlist: \"" + identifier + "\" is not a valid IP or a Steam ID"); - } + bannedPlayers.Add(new BannedPlayer(name, address, reason, expirationTime)); } } + + Save(); + File.Delete(LegacySavePath); } - public bool IsBanned(IPAddress IP, ulong steamID, ulong ownerSteamID, out string reason) + private void LoadBanList() { - reason = string.Empty; - if (IPAddress.IsLoopback(IP)) { return false; } - var bannedPlayer = bannedPlayers.Find(bp => - bp.CompareTo(IP) || - (steamID > 0 && (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)) || - (ownerSteamID > 0 && (bp.SteamID == ownerSteamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == ownerSteamID))); - reason = bannedPlayer?.Reason; - return bannedPlayer != null; - } + XDocument? doc = XMLExtensions.TryLoadXml(SavePath); + + if (doc?.Root is null) { return; } - public bool IsBanned(IPAddress IP, out string reason) - { - reason = string.Empty; - if (IPAddress.IsLoopback(IP)) { return false; } - bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); - var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP)); - reason = bannedPlayer?.Reason; - return bannedPlayer != null; - } - - public bool IsBanned(ulong steamID, out string reason) - { - reason = string.Empty; - bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); - var bannedPlayer = bannedPlayers.Find(bp => - steamID > 0 && - (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)); - reason = bannedPlayer?.Reason; - return bannedPlayer != null; - } - - public void BanPlayer(string name, IPAddress ip, string reason, TimeSpan? duration) - { - string ipStr = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4NoThrow().ToString() : ip.ToString(); - BanPlayer(name, ipStr, 0, reason, duration); - } - - public void BanPlayer(string name, string endPoint, string reason, TimeSpan? duration) - { - BanPlayer(name, endPoint, 0, reason, duration); - } - - public void BanPlayer(string name, ulong steamID, string reason, TimeSpan? duration) - { - if (steamID == 0) { return; } - BanPlayer(name, "", steamID, reason, duration); - } - - private void BanPlayer(string name, string endPoint, ulong steamID, string reason, TimeSpan? duration) - { - var existingBan = bannedPlayers.Find(bp => bp.EndPoint == endPoint && bp.SteamID == steamID); - if (existingBan != null) + static Option loadFromElement(XElement element) { - if (!duration.HasValue) return; + var accountId = AccountId.Parse(element.GetAttributeString("accountid", "")); + var address = Address.Parse(element.GetAttributeString("address", "")); - DebugConsole.Log("Set \"" + name + "\"'s ban duration to " + duration.Value); - existingBan.ExpirationTime = DateTime.Now + duration.Value; - Save(); - return; + var name = element.GetAttributeString("name", "")!; + var reason = element.GetAttributeString("reason", "")!; + DateTime? expirationTime = DateTime.FromBinary(unchecked((long)element.GetAttributeUInt64("expirationtime", 0))); + + if (expirationTime < DateTime.Now) { expirationTime = null; } + + if (accountId.IsNone() && address.IsNone()) { return Option.None(); } + + Either addressOrAccountId = accountId.TryUnwrap(out var accId) + ? (Either)accId + : address.TryUnwrap(out var addr) + ? addr + : throw new InvalidCastException(); + + return Option.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); } + + bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement) + .OfType>().Select(o => o.Value)); + } + + private void RemoveExpired() + { + bannedPlayers.RemoveAll(bp => bp.Expired); + } + + public bool IsBanned(Endpoint endpoint, out string reason) + => IsBanned(endpoint.Address, out reason); + + public bool IsBanned(Address address, out string reason) + { + RemoveExpired(); + if (address.IsLocalHost) + { + reason = string.Empty; + return false; + } + var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out Address adr) && address.Equals(adr)); + reason = bannedPlayer?.Reason ?? string.Empty; + return bannedPlayer != null; + } - System.Diagnostics.Debug.Assert(!name.Contains(',')); + public bool IsBanned(AccountId accountId, out string reason) + { + RemoveExpired(); + var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)); + reason = bannedPlayer?.Reason ?? string.Empty; + return bannedPlayer != null; + } + + public void BanPlayer(string name, Endpoint endpoint, string reason, TimeSpan? duration) + => BanPlayer(name, endpoint.Address, reason, duration); + + public void BanPlayer(string name, Either addressOrAccountId, string reason, TimeSpan? duration) + { + if (addressOrAccountId.TryGet(out Address address) && address.IsLocalHost) { return; } + + var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId); + if (existingBan != null) { bannedPlayers.Remove(existingBan); } string logMsg = "Banned " + name; - if (!string.IsNullOrEmpty(reason)) logMsg += ", reason: " + reason; - if (duration.HasValue) logMsg += ", duration: " + duration.Value.ToString(); + if (!string.IsNullOrEmpty(reason)) { logMsg += ", reason: " + reason; } + if (duration.HasValue) { logMsg += ", duration: " + duration.Value.ToString(); } DebugConsole.Log(logMsg); @@ -198,46 +179,19 @@ namespace Barotrauma.Networking expirationTime = DateTime.Now + duration.Value; } - if (!string.IsNullOrEmpty(endPoint)) - { - bannedPlayers.Add(new BannedPlayer(name, endPoint, reason, expirationTime)); - } - else if (steamID > 0) - { - bannedPlayers.Add(new BannedPlayer(name, steamID, reason, expirationTime)); - } - else - { - DebugConsole.ThrowError("Failed to ban a client (no valid IP or Steam ID given)"); - return; - } - + bannedPlayers.Add(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); Save(); } - public void UnbanPlayer(string name) + public void UnbanPlayer(Endpoint endpoint) + => UnbanPlayer(endpoint.Address); + + public void UnbanPlayer(Either addressOrAccountId) { - name = name.ToLower(); - var player = bannedPlayers.Find(bp => bp.Name.ToLower() == name); + var player = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId); if (player == null) { - DebugConsole.Log("Could not unban player \"" + name + "\". Matching player not found."); - } - else - { - RemoveBan(player); - } - } - - public void UnbanEndPoint(string endPoint) - { - ulong steamId = SteamManager.SteamIDStringToUInt64(endPoint); - var player = bannedPlayers.Find(bp => - bp.EndPoint == endPoint || - (steamId != 0 && steamId == SteamManager.SteamIDStringToUInt64(bp.EndPoint))); - if (player == null) - { - DebugConsole.Log("Could not unban endpoint \"" + endPoint + "\". Matching player not found."); + DebugConsole.Log("Could not unban endpoint \"" + addressOrAccountId + "\". Matching player not found."); } else { @@ -255,49 +209,38 @@ namespace Barotrauma.Networking Save(); } - private void RangeBan(BannedPlayer banned) - { - banned.EndPoint = ToRange(banned.EndPoint); - - BannedPlayer bp; - while ((bp = bannedPlayers.Find(x => banned.CompareTo(x.EndPoint))) != null) - { - //remove all specific bans that are now covered by the rangeban - bannedPlayers.Remove(bp); - } - - bannedPlayers.Add(banned); - - Save(); - } - public void Save() { GameServer.Log("Saving banlist", ServerLog.MessageType.ServerMessage); GameMain.Server?.ServerSettings?.UpdateFlag(ServerSettings.NetFlags.Properties); - bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); + RemoveExpired(); - List lines = new List(); - foreach (BannedPlayer banned in bannedPlayers) + static XElement saveToElement(BannedPlayer bannedPlayer) { - string line = banned.Name; - line += "," + ((banned.SteamID > 0) ? SteamManager.SteamIDUInt64ToString(banned.SteamID) : banned.EndPoint); - line += "," + (banned.ExpirationTime.HasValue ? banned.ExpirationTime.Value.ToString() : ""); - if (!string.IsNullOrWhiteSpace(banned.Reason)) line += "," + banned.Reason; + XElement retVal = new XElement("ban"); + retVal.SetAttributeValue("name", bannedPlayer.Name); + retVal.SetAttributeValue("reason", bannedPlayer.Reason); + if (bannedPlayer.AddressOrAccountId.TryGet(out AccountId accountId)) + { + retVal.SetAttributeValue("accountid", accountId.StringRepresentation); + } + else if (bannedPlayer.AddressOrAccountId.TryGet(out Address address)) + { + retVal.SetAttributeValue("address", address.StringRepresentation); + } + if (bannedPlayer.ExpirationTime is { } expirationTime) + { + retVal.SetAttributeValue("expirationtime", unchecked((ulong)expirationTime.ToBinary())); + } - lines.Add(line); + return retVal; } - try - { - File.WriteAllLines(SavePath, lines); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving the list of banned players to " + SavePath + " failed", e); - } + XDocument doc = new XDocument(new XElement("bannedplayers")); + bannedPlayers.Select(saveToElement).ForEach(doc.Root!.Add); + doc.SaveSafe(SavePath); } public void ServerAdminWrite(IWriteMessage outMsg, Client c) @@ -309,12 +252,12 @@ namespace Barotrauma.Networking if (!c.HasPermission(ClientPermissions.Ban)) { - outMsg.Write(false); outMsg.WritePadBits(); + outMsg.WriteBoolean(false); outMsg.WritePadBits(); return; } - outMsg.Write(true); - outMsg.Write(c.Connection == GameMain.Server.OwnerConnection); + outMsg.WriteBoolean(true); + outMsg.WriteBoolean(c.Connection == GameMain.Server.OwnerConnection); outMsg.WritePadBits(); outMsg.WriteVariableUInt32((UInt32)bannedPlayers.Count); @@ -322,27 +265,33 @@ namespace Barotrauma.Networking { BannedPlayer bannedPlayer = bannedPlayers[i]; - outMsg.Write(bannedPlayer.Name); - outMsg.Write(bannedPlayer.UniqueIdentifier); - outMsg.Write(bannedPlayer.IsRangeBan); - outMsg.Write(bannedPlayer.ExpirationTime != null); + outMsg.WriteString(bannedPlayer.Name); + outMsg.WriteUInt32(bannedPlayer.UniqueIdentifier); + outMsg.WriteBoolean(bannedPlayer.ExpirationTime != null); outMsg.WritePadBits(); if (bannedPlayer.ExpirationTime != null) { double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; - outMsg.Write(hoursFromNow); + outMsg.WriteDouble(hoursFromNow); } - outMsg.Write(bannedPlayer.Reason ?? ""); + outMsg.WriteString(bannedPlayer.Reason ?? ""); if (c.Connection == GameMain.Server.OwnerConnection) { - outMsg.Write(bannedPlayer.EndPoint); - outMsg.Write(bannedPlayer.SteamID); + if (bannedPlayer.AddressOrAccountId.TryGet(out Address endpoint)) + { + outMsg.WriteBoolean(true); outMsg.WritePadBits(); + outMsg.WriteString(endpoint.StringRepresentation); + } + else + { + outMsg.WriteBoolean(false); outMsg.WritePadBits(); + outMsg.WriteString(((SteamId)bannedPlayer.AddressOrAccountId).StringRepresentation); + } } } } - catch (Exception e) { string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); @@ -355,38 +304,25 @@ namespace Barotrauma.Networking { if (!c.HasPermission(ClientPermissions.Ban)) { - UInt16 removeCount = incMsg.ReadUInt16(); - incMsg.BitPosition += removeCount * 4 * 8; - UInt16 rangeBanCount = incMsg.ReadUInt16(); - incMsg.BitPosition += rangeBanCount * 4 * 8; + UInt32 removeCount = incMsg.ReadVariableUInt32(); + incMsg.BitPosition += (int)removeCount * 4 * 8; return false; } else { - UInt16 removeCount = incMsg.ReadUInt16(); + UInt32 removeCount = incMsg.ReadVariableUInt32(); for (int i = 0; i < removeCount; i++) { - UInt16 id = incMsg.ReadUInt16(); - BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); + UInt32 id = incMsg.ReadUInt32(); + BannedPlayer? bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.EndPoint + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.AddressOrAccountId + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); } } - Int16 rangeBanCount = incMsg.ReadInt16(); - for (int i = 0; i < rangeBanCount; i++) - { - UInt16 id = incMsg.ReadUInt16(); - BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); - if (bannedPlayer != null) - { - GameServer.Log(GameServer.ClientLogName(c) + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.EndPoint + ")", ServerLog.MessageType.ConsoleUsage); - RangeBan(bannedPlayer); - } - } - return removeCount > 0 || rangeBanCount > 0; + return removeCount > 0; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 2c486b2ea..0fb07b70a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -212,24 +212,24 @@ namespace Barotrauma.Networking public virtual void ServerWrite(IWriteMessage msg, Client c) { - msg.Write((byte)ServerNetObject.CHAT_MESSAGE); - msg.Write(NetStateID); + msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); - msg.Write((byte)ChangeType); - msg.Write(Text); + msg.WriteByte((byte)ChangeType); + msg.WriteString(Text); - msg.Write(SenderName); - msg.Write(SenderClient != null); + msg.WriteString(SenderName); + msg.WriteBoolean(SenderClient != null); if (SenderClient != null) { - msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + msg.WriteString(SenderClient.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : SenderClient.SessionId.ToString()); } - msg.Write(Sender != null && c.InGame); + msg.WriteBoolean(Sender != null && c.InGame); if (Sender != null && c.InGame) { - msg.Write(Sender.ID); + msg.WriteUInt16(Sender.ID); } - msg.Write(customTextColor != null); + msg.WriteBoolean(customTextColor != null); if (customTextColor != null) { msg.WriteColorR8G8B8A8(customTextColor.Value); @@ -237,7 +237,7 @@ namespace Barotrauma.Networking msg.WritePadBits(); if (Type == ChatMessageType.ServerMessageBoxInGame) { - msg.Write(IconStyle); + msg.WriteString(IconStyle); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index fe720421c..fdea971a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -8,12 +8,16 @@ namespace Barotrauma.Networking { public bool VoiceEnabled = true; - public UInt16 LastRecvClientListUpdate = 0; + public UInt16 LastRecvClientListUpdate + = NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID); - public UInt16 LastSentServerSettingsUpdate = 0; - public UInt16 LastRecvServerSettingsUpdate = 0; + public UInt16 LastSentServerSettingsUpdate + = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); + public UInt16 LastRecvServerSettingsUpdate + = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); - public UInt16 LastRecvLobbyUpdate = 0; + public UInt16 LastRecvLobbyUpdate + = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); public UInt16 LastSentChatMsgID = 0; //last msg this client said public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about @@ -21,7 +25,8 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID = 0; public UInt16 LastRecvEntityEventID = 0; - public readonly Dictionary LastRecvCampaignUpdate = new Dictionary(); + public readonly Dictionary LastRecvCampaignUpdate + = new Dictionary(); public UInt16 LastRecvCampaignSave = 0; public (UInt16 saveId, float time) LastCampaignSaveSendTime; @@ -57,11 +62,13 @@ namespace Barotrauma.Networking public bool ReadyToStart; - public List JobPreferences; + public List JobPreferences { get; set; } public JobVariant AssignedJob; public float DeleteDisconnectedTimer; + public DateTime JoinTime; + private CharacterInfo characterInfo; public CharacterInfo CharacterInfo { @@ -105,15 +112,26 @@ namespace Barotrauma.Networking } } + private List kickVoters; + + public int KickVoteCount + { + get { return kickVoters.Count; } + } + partial void InitProjSpecific() { + kickVoters = new List(); + JobPreferences = new List(); - VoipQueue = new VoipQueue(ID, true, true); + VoipQueue = new VoipQueue(SessionId, true, true); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); //initialize to infinity, gets set to a proper value when initializing midround syncing MidRoundSyncTimeOut = double.PositiveInfinity; + + JoinTime = DateTime.Now; } partial void DisposeProjSpecific() @@ -147,7 +165,22 @@ namespace Barotrauma.Networking { if (string.IsNullOrWhiteSpace(name)) { return false; } - char[] disallowedChars = new char[] { ';', ',', '<', '>', '/', '\\', '[', ']', '"', '?' }; + char[] disallowedChars = + { + //',', //previously disallowed because of the ban list format + + ';', + '<', + '>', + + '/', //disallowed because of server messages using forward slash as a delimiter (TODO: implement escaping) + + '\\', + '[', + ']', + '"', + '?' + }; if (name.Any(c => disallowedChars.Contains(c))) { return false; } foreach (char character in name) @@ -158,34 +191,88 @@ namespace Barotrauma.Networking return true; } - public bool EndpointMatches(string endPoint) + public bool AddressMatches(Address address) { - return Connection.EndpointMatches(endPoint); + return Connection.Endpoint.Address.Equals(address); } + public void AddKickVote(Client voter) + { + if (voter != null && !kickVoters.Contains(voter)) { kickVoters.Add(voter); } + } + + public void RemoveKickVote(Client voter) + { + kickVoters.Remove(voter); + } + + public bool HasKickVoteFrom(Client voter) + { + return kickVoters.Contains(voter); + } + + public bool HasKickVoteFromSessionId(int id) + { + return kickVoters.Any(k => k.SessionId == id); + } + + public static void UpdateKickVotes(IReadOnlyList connectedClients) + { + foreach (Client client in connectedClients) + { + client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter)); + } + } + + /// + /// Reset what this client has voted for and the kick votes given to this client + /// + public void ResetVotes(bool resetKickVotes) + { + for (int i = 0; i < votes.Length; i++) + { + votes[i] = null; + } + if (resetKickVotes) + { + kickVoters.Clear(); + } + } + + public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) { - this.Permissions = permissions; - this.PermittedConsoleCommands.Clear(); - foreach (var command in permittedConsoleCommands) + Permissions = permissions; + PermittedConsoleCommands.Clear(); + PermittedConsoleCommands.UnionWith(permittedConsoleCommands); + if (Permissions.HasFlag(ClientPermissions.ManageSettings)) { - this.PermittedConsoleCommands.Add(command); + //ensure the client has the up-to-date server settings + GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); } } public void GivePermission(ClientPermissions permission) { - if (!this.Permissions.HasFlag(permission)) this.Permissions |= permission; + if (!Permissions.HasFlag(permission)) + { + Permissions |= permission; + if (permission.HasFlag(ClientPermissions.ManageSettings)) + { + //ensure the client has the up-to-date server settings + GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); + } + } } public void RemovePermission(ClientPermissions permission) { - this.Permissions &= ~permission; + Permissions &= ~permission; } public bool HasPermission(ClientPermissions permission) { - return this.Permissions.HasFlag(permission); + return Permissions.HasFlag(permission); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index 146ae81c0..0b4937265 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -32,23 +32,23 @@ namespace Barotrauma if (GameMain.Server is null) { return; } if (!(extraData is SpawnOrRemove entities)) { throw new Exception($"Malformed {nameof(EntitySpawner)} event: expected {nameof(SpawnOrRemove)}"); } - message.Write(entities is RemoveEntity); + message.WriteBoolean(entities is RemoveEntity); if (entities is RemoveEntity) { - message.Write(entities.ID); + message.WriteUInt16(entities.ID); } else { switch (entities.Entity) { case Item item: - message.Write((byte)SpawnableType.Item); + message.WriteByte((byte)SpawnableType.Item); DebugConsole.Log( $"Writing item spawn data {item} (ID: {entities.ID})"); item.WriteSpawnData(message, entities.ID, entities.InventoryID, entities.ItemContainerIndex, entities.SlotIndex); break; case Character character: - message.Write((byte)SpawnableType.Character); + message.WriteByte((byte)SpawnableType.Character); DebugConsole.Log( $"Writing character spawn data: {character} (ID: {entities.ID})"); character.WriteSpawnData(message, entities.ID, restrictMessageSize: true); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index a8278b766..bb9681537 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking } } - public static int MaxPacketsPerUpdate = 4; + public static int MaxPacketsPerUpdate = 10; public float PacketsPerUpdate { get; set; } = 1.0f; public byte[] Data { get; } @@ -219,27 +219,27 @@ namespace Barotrauma.Networking if (!transfer.Acknowledged) { message = new WriteOnlyMessage(); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); + message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); //if the recipient is the owner of the server (= a client running the server from the main exe) //we don't need to send anything, the client can just read the file directly if (transfer.Connection == GameMain.Server.OwnerConnection) { - message.Write((byte)FileTransferMessageType.TransferOnSameMachine); - message.Write((byte)transfer.ID); - message.Write((byte)transfer.FileType); - message.Write(transfer.FilePath); + message.WriteByte((byte)FileTransferMessageType.TransferOnSameMachine); + message.WriteByte((byte)transfer.ID); + message.WriteByte((byte)transfer.FileType); + message.WriteString(transfer.FilePath); peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); transfer.Status = FileTransferStatus.Finished; } else { - message.Write((byte)FileTransferMessageType.Initiate); - message.Write((byte)transfer.ID); - message.Write((byte)transfer.FileType); + message.WriteByte((byte)FileTransferMessageType.Initiate); + message.WriteByte((byte)transfer.ID); + message.WriteByte((byte)transfer.FileType); //message.Write((ushort)chunkLen); - message.Write(transfer.Data.Length); - message.Write(transfer.FileName); + message.WriteInt32(transfer.Data.Length); + message.WriteString(transfer.FileName); peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); transfer.Status = FileTransferStatus.Sending; @@ -262,13 +262,13 @@ namespace Barotrauma.Networking int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); message = new WriteOnlyMessage(); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - message.Write((byte)FileTransferMessageType.Data); + message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); + message.WriteByte((byte)FileTransferMessageType.Data); - message.Write((byte)transfer.ID); - message.Write(transfer.SentOffset); + message.WriteByte((byte)transfer.ID); + message.WriteInt32(transfer.SentOffset); - message.Write((ushort)sendByteCount); + message.WriteUInt16((ushort)sendByteCount); int chunkDestPos = message.BytePosition; message.BitPosition += sendByteCount * 8; message.LengthBits = Math.Max(message.LengthBits, message.BitPosition); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 54bbd573c..318f77ff8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Networking string resultFileName = dir.StartsWith(ContentPackage.LocalModsDir) ? $"Local_{mod.Name}" - : $"Workshop_{mod.Name}_{mod.SteamWorkshopId}"; + : $"Workshop_{mod.Name}_{(mod.UgcId.TryUnwrap(out var ugcId) ? ugcId.ToString() : "NULL")}"; resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 70b800d67..e220e08a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1,5 +1,4 @@ -#define ALLOW_BOT_TRAITORS -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Barotrauma.Steam; @@ -17,9 +16,12 @@ using System.Net; namespace Barotrauma.Networking { - partial class GameServer : NetworkMember + sealed class GameServer : NetworkMember { public override bool IsServer => true; + public override bool IsClient => false; + + public override Voting Voting { get; } private string serverName; public string ServerName @@ -28,7 +30,8 @@ namespace Barotrauma.Networking set { if (string.IsNullOrEmpty(value)) { return; } - serverName = value.Replace(":", "").Replace(";", ""); + + serverName = value; } } @@ -52,7 +55,7 @@ namespace Barotrauma.Networking public ServerPeer ServerPeer { get { return serverPeer; } } private DateTime refreshMasterTimer; - private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); + private readonly TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); private bool registeredToMaster; private DateTime roundStartTime; @@ -60,6 +63,11 @@ namespace Barotrauma.Networking private bool autoRestartTimerRunning; private float endRoundTimer; + /// + /// Chat messages that get sent to the owner of the server when the owner is determined + /// + private static readonly Queue pendingMessagesToOwner = new Queue(); + public VoipServer VoipServer { get; @@ -87,7 +95,7 @@ namespace Barotrauma.Networking } #endif - public override List ConnectedClients + public override IReadOnlyList ConnectedClients { get { @@ -101,24 +109,27 @@ namespace Barotrauma.Networking get { return entityEventManager; } } - public TimeSpan UpdateInterval - { - get { return updateInterval; } - } - - public int Port => serverSettings?.Port ?? 0; + public int Port => ServerSettings?.Port ?? 0; //only used when connected to steam - public int QueryPort => serverSettings?.QueryPort ?? 0; + public int QueryPort => ServerSettings?.QueryPort ?? 0; public NetworkConnection OwnerConnection { get; private set; } - private readonly int? ownerKey; - private readonly UInt64? ownerSteamId; + private readonly Option ownerKey; + private readonly Option ownerSteamId; - public GameServer(string name, IPAddress listenIp, int port, int queryPort = 0, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10, int? ownKey = null, UInt64? steamId = null) + public GameServer( + string name, + IPAddress listenIp, + int port, + int queryPort, + bool isPublic, + string password, + bool attemptUPnP, + int maxPlayers, + Option ownerKey, + Option ownerSteamId) { - name = name.Replace(":", ""); - name = name.Replace(";", ""); if (name.Length > NetConfig.ServerNameMaxLength) { name = name.Substring(0, NetConfig.ServerNameMaxLength); @@ -128,104 +139,87 @@ namespace Barotrauma.Networking LastClientListUpdateID = 0; - serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP, listenIp); - KarmaManager.SelectPreset(serverSettings.KarmaPreset); - serverSettings.SetPassword(password); + ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP, listenIp); + KarmaManager.SelectPreset(ServerSettings.KarmaPreset); + ServerSettings.SetPassword(password); Voting = new Voting(); - ownerKey = ownKey; + this.ownerKey = ownerKey; - ownerSteamId = steamId; + this.ownerSteamId = ownerSteamId; entityEventManager = new ServerEntityEventManager(this); - - CoroutineManager.StartCoroutine(StartServer(isPublic)); } - private IEnumerable StartServer(bool isPublic) + public void StartServer() { - bool error = false; - try + Log("Starting the server...", ServerLog.MessageType.ServerMessage); + + var callbacks = new ServerPeer.Callbacks( + ReadDataMessage, + OnClientDisconnect, + OnInitializationComplete, + GameMain.Instance.CloseServer, + OnOwnerDetermined); + + if (ownerSteamId.TryUnwrap(out var steamId)) { - Log("Starting the server...", ServerLog.MessageType.ServerMessage); - if (!ownerSteamId.HasValue || ownerSteamId.Value == 0) - { - Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); - serverPeer = new LidgrenServerPeer(ownerKey, serverSettings); - } - else - { - Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, ownerKey.Value, serverSettings); - } - - serverPeer.OnInitializationComplete = OnInitializationComplete; - serverPeer.OnMessageReceived = ReadDataMessage; - serverPeer.OnDisconnect = OnClientDisconnect; - serverPeer.OnShutdown = GameMain.Instance.CloseServer; - serverPeer.OnOwnerDetermined = OnOwnerDetermined; - - FileSender = new FileSender(serverPeer, MsgConstants.MTU); - FileSender.OnEnded += FileTransferChanged; - FileSender.OnStarted += FileTransferChanged; - - if (serverSettings.AllowModDownloads) { ModSender = new ModSender(); } - - serverPeer.Start(); - - VoipServer = new VoipServer(serverPeer); + Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); + serverPeer = new SteamP2PServerPeer(steamId, ownerKey.Fallback(0), ServerSettings, callbacks); } - catch (Exception e) + else { - Log("Error while starting the server (" + e.Message + ")", ServerLog.MessageType.Error); - - System.Net.Sockets.SocketException socketException = e as System.Net.Sockets.SocketException; - - error = true; + Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); + serverPeer = new LidgrenServerPeer(ownerKey, ServerSettings, callbacks); } - if (error) - { - if (serverPeer != null) serverPeer.Close("Error while starting the server"); + FileSender = new FileSender(serverPeer, MsgConstants.MTU); + FileSender.OnEnded += FileTransferChanged; + FileSender.OnStarted += FileTransferChanged; - Environment.Exit(-1); + if (ServerSettings.AllowModDownloads) { ModSender = new ModSender(); } - yield return CoroutineStatus.Success; - } + serverPeer.Start(); + VoipServer = new VoipServer(serverPeer); if (serverPeer is LidgrenServerPeer) { #if USE_STEAM - registeredToMaster = SteamManager.CreateServer(this, isPublic); + registeredToMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); #endif } GameMain.LuaCs.Initialize(); - TickRate = serverSettings.TickRate; - Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); GameMain.NetLobbyScreen.RandomizeSettings(); - if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) + if (!string.IsNullOrEmpty(ServerSettings.SelectedSubmarine)) { - SubmarineInfo sub = SubmarineInfo.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)) + if (!string.IsNullOrEmpty(ServerSettings.SelectedShuttle)) { - SubmarineInfo shuttle = SubmarineInfo.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; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); + } - yield return CoroutineStatus.Success; + + /// + /// Creates a message that gets sent to the server owner once the connection is initialized. Can be used to for example notify the owner of problems during initialization + /// + public static void AddPendingMessageToOwner(string message, ChatMessageType messageType) + { + pendingMessagesToOwner.Enqueue(ChatMessage.Create(string.Empty, message, messageType, sender: null)); } private void OnOwnerDetermined(NetworkConnection connection) @@ -247,27 +241,25 @@ namespace Barotrauma.Networking var tempList = ConnectedClients.Where(c => c.Connection != OwnerConnection).ToList(); foreach (var c in tempList) { - DisconnectClient(c.Connection, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + DisconnectClient(c.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); } if (OwnerConnection != null) { var conn = OwnerConnection; OwnerConnection = null; - DisconnectClient(conn, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + DisconnectClient(conn, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); } Thread.Sleep(500); } - private void OnInitializationComplete(NetworkConnection connection) + private void OnInitializationComplete(NetworkConnection connection, string clientName) { - string clName = connection.Name; - Client newClient = new Client(clName, GetNewClientID()); + Client newClient = new Client(clientName, GetNewClientSessionId()); newClient.InitClientSync(); newClient.Connection = connection; newClient.Connection.Status = NetworkConnectionStatus.Connected; - newClient.SteamID = connection.SteamID; - newClient.OwnerSteamID = connection.OwnerSteamID; + newClient.AccountInfo = connection.AccountInfo; newClient.Language = connection.Language; - ConnectedClients.Add(newClient); + connectedClients.Add(newClient); var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient)); if (previousPlayer != null) @@ -295,9 +287,8 @@ namespace Barotrauma.Networking GameMain.LuaCs.Hook.Call("client.connected", newClient); - - SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); - serverSettings.ServerDetailsChanged = true; + SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); + ServerSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { @@ -306,10 +297,10 @@ namespace Barotrauma.Networking previousPlayer.Name = newClient.Name; } - var savedPermissions = serverSettings.ClientPermissions.Find(cp => - cp.SteamID > 0 ? - cp.SteamID == newClient.SteamID : - newClient.EndpointMatches(cp.EndPoint)); + var savedPermissions = ServerSettings.ClientPermissions.Find(scp => + scp.AddressOrAccountId.TryGet(out AccountId accountId) + ? newClient.AccountId.ValueEquals(accountId) + : newClient.Connection.Endpoint.Address == scp.AddressOrAccountId); if (savedPermissions != null) { @@ -324,7 +315,7 @@ namespace Barotrauma.Networking } else { - newClient.SetPermissions(ClientPermissions.None, new List()); + newClient.SetPermissions(ClientPermissions.None, Enumerable.Empty()); } } @@ -337,14 +328,14 @@ namespace Barotrauma.Networking } } - private void OnClientDisconnect(NetworkConnection connection, string disconnectMsg) + private void OnClientDisconnect(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket) { Client connectedClient = connectedClients.Find(c => c.Connection == connection); - DisconnectClient(connectedClient, reason: disconnectMsg); + DisconnectClient(connectedClient, peerDisconnectPacket); } - public override void Update(float deltaTime) + public void Update(float deltaTime) { #if CLIENT if (ShowNetStats) { netStats.Update(deltaTime); } @@ -353,25 +344,23 @@ namespace Barotrauma.Networking if (OwnerConnection != null && ChildServerRelay.HasShutDown) { - Disconnect(); + Quit(); return; } - base.Update(deltaTime); - FileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); UpdatePing(); - if (serverSettings.VoiceChatEnabled) + if (ServerSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); } - if (gameStarted) + if (GameStarted) { - respawnManager?.Update(deltaTime); + RespawnManager?.Update(deltaTime); entityEventManager.Update(connectedClients); @@ -387,16 +376,16 @@ namespace Barotrauma.Networking character.SetStun(1.0f); } - Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.EndpointMatches(character.OwnerClientEndPoint)); + Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); - if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) + if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); continue; } if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!serverSettings.AllowSpectating || !owner.SpectateOnly)) + (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) { SetClientCharacter(owner, character); } @@ -442,7 +431,7 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (serverSettings.AutoRestart && isCrewDead) + else if (ServerSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -452,7 +441,7 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (isCrewDead && respawnManager == null) + else if (isCrewDead && RespawnManager == null) { #if !DEBUG if (endRoundTimer <= 0.0f) @@ -481,7 +470,7 @@ namespace Barotrauma.Networking { Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); } - else if (serverSettings.AutoRestart && isCrewDead) + else if (ServerSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } @@ -489,7 +478,7 @@ namespace Barotrauma.Networking { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } - else if (respawnManager == null) + else if (RespawnManager == null) { Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); } @@ -507,7 +496,7 @@ namespace Barotrauma.Networking // -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine)) { - if (serverSettings.AutoRestart) serverSettings.AutoRestartTimer = Math.Max(serverSettings.AutoRestartInterval, 5.0f); + if (ServerSettings.AutoRestart) ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f); //GameMain.NetLobbyScreen.StartButtonEnabled = true; GameMain.NetLobbyScreen.LastUpdateID++; @@ -516,15 +505,15 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame && + else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) { - if (serverSettings.AutoRestart) + if (ServerSettings.AutoRestart) { //autorestart if there are any non-spectators on the server (ignoring the server owner) bool shouldAutoRestart = connectedClients.Any(c => c.Connection != OwnerConnection && - (!c.SpectateOnly || !serverSettings.AllowSpectating)); + (!c.SpectateOnly || !ServerSettings.AllowSpectating)); if (shouldAutoRestart != autoRestartTimerRunning) { @@ -534,18 +523,18 @@ namespace Barotrauma.Networking if (autoRestartTimerRunning) { - serverSettings.AutoRestartTimer -= deltaTime; + ServerSettings.AutoRestartTimer -= deltaTime; } } - if (serverSettings.AutoRestart && autoRestartTimerRunning && serverSettings.AutoRestartTimer < 0.0f) + if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) { StartGame(); } - else if (serverSettings.StartWhenClientsReady) + else if (ServerSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); - if (clientsReady / (float)connectedClients.Count >= serverSettings.StartWhenClientsReadyRatio) + if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) { StartGame(); } @@ -557,7 +546,7 @@ namespace Barotrauma.Networking disconnectedClients[i].DeleteDisconnectedTimer -= deltaTime; if (disconnectedClients[i].DeleteDisconnectedTimer > 0.0f) continue; - if (gameStarted && disconnectedClients[i].Character != null) + if (GameStarted && disconnectedClients[i].Character != null) { disconnectedClients[i].Character.Kill(CauseOfDeathType.Disconnected, null); disconnectedClients[i].Character = null; @@ -573,16 +562,16 @@ 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.IsIncapacitated) + if (GameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { if (c.Connection != OwnerConnection && c.Permissions != ClientPermissions.All) { c.KickAFKTimer += deltaTime; } } } - if (connectedClients.Any(c => c.KickAFKTimer >= serverSettings.KickAFKTime)) + if (connectedClients.Any(c => c.KickAFKTimer >= ServerSettings.KickAFKTime)) { IEnumerable kickAFK = connectedClients.FindAll(c => - c.KickAFKTimer >= serverSettings.KickAFKTime && + c.KickAFKTimer >= ServerSettings.KickAFKTime && (OwnerConnection == null || c.Connection != OwnerConnection)); foreach (Client c in kickAFK) { @@ -642,10 +631,10 @@ namespace Barotrauma.Networking } } - updateTimer = DateTime.Now + updateInterval; + updateTimer = DateTime.Now + UpdateInterval; } - if (registeredToMaster && (DateTime.Now > refreshMasterTimer || serverSettings.ServerDetailsChanged)) + if (registeredToMaster && (DateTime.Now > refreshMasterTimer || ServerSettings.ServerDetailsChanged)) { if (GameSettings.CurrentConfig.UseSteamMatchmaking) { @@ -658,7 +647,7 @@ namespace Barotrauma.Networking } } refreshMasterTimer = DateTime.Now + refreshMasterInterval; - serverSettings.ServerDetailsChanged = false; + ServerSettings.ServerDetailsChanged = false; } } @@ -679,18 +668,18 @@ namespace Barotrauma.Networking ConnectedClients.ForEach(c => { IWriteMessage pingReq = new WriteOnlyMessage(); - pingReq.Write((byte)ServerPacketHeader.PING_REQUEST); - pingReq.Write((byte)lastPingData.Length); - pingReq.Write(lastPingData, 0, lastPingData.Length); + pingReq.WriteByte((byte)ServerPacketHeader.PING_REQUEST); + pingReq.WriteByte((byte)lastPingData.Length); + pingReq.WriteBytes(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); + pingInf.WriteByte((byte)ServerPacketHeader.CLIENT_PINGS); + pingInf.WriteByte((byte)ConnectedClients.Count); ConnectedClients.ForEach(c2 => { - pingInf.Write(c2.ID); - pingInf.Write(c2.Ping); + pingInf.WriteByte(c2.SessionId); + pingInf.WriteUInt16(c2.Ping); }); serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable); }); @@ -724,7 +713,7 @@ namespace Barotrauma.Networking UpdateCharacterInfo(inc, connectedClient); //game already started -> send start message immediately - if (gameStarted) + if (GameStarted) { SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient, true); } @@ -735,7 +724,7 @@ namespace Barotrauma.Networking { DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message."); } - else if (!gameStarted) + else if (!GameStarted) { DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message."); } @@ -748,7 +737,7 @@ namespace Barotrauma.Networking ClientReadLobby(inc); break; case ClientPacketHeader.UPDATE_INGAME: - if (!gameStarted) { return; } + if (!GameStarted) { return; } ClientReadIngame(inc); break; case ClientPacketHeader.CAMPAIGN_SETUP_INFO: @@ -763,7 +752,7 @@ namespace Barotrauma.Networking var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); - if (gameStarted) + if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; @@ -789,7 +778,7 @@ namespace Barotrauma.Networking else { string saveName = inc.ReadString(); - if (gameStarted) + if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; @@ -798,14 +787,14 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.VOICE: - if (serverSettings.VoiceChatEnabled && !connectedClient.Muted) + if (ServerSettings.VoiceChatEnabled && !connectedClient.Muted) { byte id = inc.ReadByte(); - if (connectedClient.ID != id) + if (connectedClient.SessionId != id) { #if DEBUG DebugConsole.ThrowError( - "Client \"" + connectedClient.Name + "\" sent a VOIP update that didn't match its ID (" + id.ToString() + "!=" + connectedClient.ID.ToString() + ")"); + "Client \"" + connectedClient.Name + "\" sent a VOIP update that didn't match its ID (" + id.ToString() + "!=" + connectedClient.SessionId.ToString() + ")"); #endif return; } @@ -813,7 +802,7 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.SERVER_SETTINGS: - serverSettings.ServerRead(inc, connectedClient); + ServerSettings.ServerRead(inc, connectedClient); break; case ClientPacketHeader.SERVER_COMMAND: ClientReadServerCommand(inc); @@ -837,7 +826,7 @@ namespace Barotrauma.Networking ReadReadyToSpawnMessage(inc, connectedClient); break; case ClientPacketHeader.FILE_REQUEST: - if (serverSettings.AllowFileTransfers) + if (ServerSettings.AllowFileTransfers) { FileSender.ReadFileRequest(inc, connectedClient); } @@ -894,7 +883,7 @@ namespace Barotrauma.Networking { errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } - if (gameStarted) + if (GameStarted) { var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) @@ -1023,14 +1012,14 @@ namespace Barotrauma.Networking entityEventManager.CreateEvent(serverSerializable, extraData); } - private byte GetNewClientID() + private byte GetNewClientSessionId() { - byte userID = 1; - while (connectedClients.Any(c => c.ID == userID)) + byte userId = 1; + while (connectedClients.Any(c => c.SessionId == userId)) { - userID++; + userId++; } - return userID; + return userId; } private void ClientReadLobby(IReadMessage inc) @@ -1114,7 +1103,7 @@ namespace Barotrauma.Networking bool midroundSyncingDone = inc.ReadBoolean(); inc.ReadPadBits(); - if (gameStarted) + if (GameStarted) { if (!c.InGame) { @@ -1177,7 +1166,13 @@ namespace Barotrauma.Networking c.LastRecvEntityEventID = lastRecvEntityEventID; DebugConsole.Log("Finished midround syncing " + c.Name + " - switching from ID " + prevID + " to " + c.LastRecvEntityEventID); //notify the client of the state of the respawn manager (so they show the respawn prompt if needed) - if (respawnManager != null) { CreateEntityEvent(respawnManager); } + if (RespawnManager != null) { CreateEntityEvent(RespawnManager); } + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) + { + //notify the client of the current bank balance and purchased repairs + campaign.Bank.ForceUpdate(); + campaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.Misc); + } } else { @@ -1202,7 +1197,7 @@ namespace Barotrauma.Networking { //give midround-joining clients a bit more time to get in sync if they keep receiving messages int receivedEventCount = lastRecvEntityEventID - c.LastRecvEntityEventID; - if (receivedEventCount < 0) receivedEventCount += ushort.MaxValue; + if (receivedEventCount < 0) { receivedEventCount += ushort.MaxValue; } c.MidRoundSyncTimeOut += receivedEventCount * 0.01f; DebugConsole.Log("Midround sync timeout " + c.MidRoundSyncTimeOut.ToString("0.##") + "/" + Timing.TotalTime.ToString("0.##")); } @@ -1254,7 +1249,7 @@ namespace Barotrauma.Networking } //don't read further messages if the client has been disconnected (kicked due to spam for example) - if (!connectedClients.Contains(c)) break; + if (!connectedClients.Contains(c)) { break; } } } @@ -1292,7 +1287,7 @@ namespace Barotrauma.Networking private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { - sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); sender.WaitForNextRoundRespawn = inc.ReadBoolean(); if (!(GameMain.GameSession?.GameMode is CampaignMode)) { @@ -1354,7 +1349,6 @@ namespace Barotrauma.Networking case ClientPermissions.Ban: string bannedName = inc.ReadString().ToLowerInvariant(); string banReason = inc.ReadString(); - bool range = inc.ReadBoolean(); double durationSeconds = inc.ReadDouble(); TimeSpan? banDuration = null; @@ -1364,7 +1358,7 @@ namespace Barotrauma.Networking if (bannedClient != null) { Log("Client \"" + ClientLogName(sender) + "\" banned \"" + ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage); - BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); + BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration); } else { @@ -1372,7 +1366,7 @@ namespace Barotrauma.Networking if (bannedPreviousClient != null) { Log("Client \"" + ClientLogName(sender) + "\" banned \"" + bannedPreviousClient.Name + "\".", ServerLog.MessageType.ServerMessage); - BanPreviousPlayer(bannedPreviousClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); + BanPreviousPlayer(bannedPreviousClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration); } else { @@ -1381,9 +1375,16 @@ namespace Barotrauma.Networking } break; case ClientPermissions.Unban: - string unbannedName = inc.ReadString(); - string unbannedIP = inc.ReadString(); - UnbanPlayer(unbannedName, unbannedIP); + bool isPlayerName = inc.ReadBoolean(); inc.ReadPadBits(); + string str = inc.ReadString(); + if (isPlayerName) + { + UnbanPlayer(playerName: str); + } + else if (Endpoint.Parse(str).TryUnwrap(out var endpoint)) + { + UnbanPlayer(endpoint); + } break; case ClientPermissions.ManageRound: bool end = inc.ReadBoolean(); @@ -1394,7 +1395,7 @@ namespace Barotrauma.Networking mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) { bool save = inc.ReadBoolean(); - if (gameStarted) + if (GameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) @@ -1416,7 +1417,7 @@ namespace Barotrauma.Networking bool continueCampaign = inc.ReadBoolean(); if (mpCampaign != null && mpCampaign.GameOver || continueCampaign) { - if (gameStarted) + if (GameStarted) { SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error); break; @@ -1427,7 +1428,7 @@ namespace Barotrauma.Networking } } - else if (!gameStarted && !initiatedStartGame) + else if (!GameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); @@ -1470,21 +1471,22 @@ namespace Barotrauma.Networking case ClientPermissions.SelectSub: bool isShuttle = inc.ReadBoolean(); inc.ReadPadBits(); - UInt16 subIndex = inc.ReadUInt16(); + string subHash = inc.ReadString(); var subList = GameMain.NetLobbyScreen.GetSubList(); - if (subIndex >= subList.Count) + var sub = GameMain.NetLobbyScreen.GetSubList().FirstOrDefault(s => s.MD5Hash.StringRepresentation == subHash); + if (sub == null) { - DebugConsole.NewMessage($"Client \"{ClientLogName(sender)}\" attempted to select a sub, index out of bounds ({subIndex})", Color.Red); + DebugConsole.NewMessage($"Client \"{ClientLogName(sender)}\" attempted to select a sub, could not find a sub with the MD5 hash \"{subHash}\".", Color.Red); } else { if (isShuttle) { - GameMain.NetLobbyScreen.SelectedShuttle = subList[subIndex]; + GameMain.NetLobbyScreen.SelectedShuttle = sub; } else { - GameMain.NetLobbyScreen.SelectedSub = subList[subIndex]; + GameMain.NetLobbyScreen.SelectedSub = sub; } } break; @@ -1498,11 +1500,11 @@ namespace Barotrauma.Networking const int MaxSaves = 255; var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.Write((byte)Math.Min(saveInfos.Count, MaxSaves)); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) { - msg.Write(saveInfos[i]); + msg.WriteNetSerializableStruct(saveInfos[i]); } serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); } @@ -1519,7 +1521,7 @@ namespace Barotrauma.Networking break; case ClientPermissions.ManagePermissions: byte targetClientID = inc.ReadByte(); - Client targetClient = connectedClients.Find(c => c.ID == targetClientID); + Client targetClient = connectedClients.Find(c => c.SessionId == targetClientID); if (targetClient == null || targetClient == sender || targetClient.Connection == OwnerConnection) { return; } targetClient.ReadPermissions(inc); @@ -1556,7 +1558,7 @@ namespace Barotrauma.Networking private void ClientWrite(Client c) { - if (gameStarted && c.InGame) + if (GameStarted && c.InGame) { ClientWriteIngame(c); } @@ -1564,7 +1566,7 @@ namespace Barotrauma.Networking { //if 30 seconds have passed since the round started and the client isn't ingame yet, //consider the client's character disconnected (causing it to die if the client does not join soon) - if (gameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f) + if (GameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f) { c.Character.ClientDisconnected = true; } @@ -1572,6 +1574,15 @@ namespace Barotrauma.Networking ClientWriteLobby(c); } + + if (c.Connection == OwnerConnection) + { + while (pendingMessagesToOwner.Any()) + { + SendDirectChatMessage(pendingMessagesToOwner.Dequeue(), c); + } + } + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && GameMain.NetLobbyScreen.SelectedMode == campaign.Preset && NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) @@ -1605,21 +1616,22 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); } - outmsg.Write(c.ID); + outmsg.WriteByte(c.SessionId); var subList = GameMain.NetLobbyScreen.GetSubList(); - outmsg.Write((UInt16)subList.Count); + outmsg.WriteUInt16((UInt16)subList.Count); for (int i = 0; i < subList.Count; i++) { var sub = subList[i]; - outmsg.Write(sub.Name); - outmsg.Write(sub.MD5Hash.ToString()); - outmsg.Write((byte)sub.SubmarineClass); - outmsg.Write(sub.RequiredContentPackagesInstalled); + outmsg.WriteString(sub.Name); + outmsg.WriteString(sub.MD5Hash.ToString()); + outmsg.WriteByte((byte)sub.SubmarineClass); + outmsg.WriteBoolean(sub.HasTag(SubmarineTag.Shuttle)); + outmsg.WriteBoolean(sub.RequiredContentPackagesInstalled); } - outmsg.Write(GameStarted); - outmsg.Write(serverSettings.AllowSpectating); + outmsg.WriteBoolean(GameStarted); + outmsg.WriteBoolean(ServerSettings.AllowSpectating); c.WritePermissions(outmsg); } @@ -1692,23 +1704,23 @@ namespace Barotrauma.Networking } IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); + outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.Write((float)NetTime.Now); + outmsg.WriteSingle((float)NetTime.Now); - outmsg.Write((byte)ServerNetObject.SYNC_IDS); - outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server - outmsg.Write(c.LastSentEntityEventID); + outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + outmsg.WriteUInt16(c.LastSentEntityEventID); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { - outmsg.Write(true); + outmsg.WriteBoolean(true); outmsg.WritePadBits(); campaign.ServerWrite(outmsg, c); } else { - outmsg.Write(false); + outmsg.WriteBoolean(false); outmsg.WritePadBits(); } @@ -1734,8 +1746,8 @@ namespace Barotrauma.Networking } IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.Write(entity is Item); tempBuffer.WritePadBits(); - tempBuffer.Write(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); + tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); + tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); entityPositionSync.ServerWritePosition(tempBuffer, c); //no more room in this packet @@ -1744,9 +1756,9 @@ namespace Barotrauma.Networking break; } - outmsg.Write((byte)ServerNetObject.ENTITY_POSITION); + outmsg.WriteByte((byte)ServerNetObject.ENTITY_POSITION); outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); c.PositionUpdateLastSent[entity] = (float)NetTime.Now; @@ -1754,7 +1766,7 @@ namespace Barotrauma.Networking } positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; - outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { @@ -1774,8 +1786,8 @@ namespace Barotrauma.Networking for (int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++) { outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.Write((float)Lidgren.Network.NetTime.Now); + outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); + outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now); int eventManagerBytes = outmsg.LengthBytes; entityEventManager.Write(c, outmsg, out List sentEvents); @@ -1786,7 +1798,7 @@ namespace Barotrauma.Networking break; } - outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { @@ -1816,34 +1828,30 @@ namespace Barotrauma.Networking bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) { return; } - outmsg.Write((byte)ServerNetObject.CLIENT_LIST); - outmsg.Write(LastClientListUpdateID); + outmsg.WriteByte((byte)ServerNetObject.CLIENT_LIST); + outmsg.WriteUInt16(LastClientListUpdateID); GameMain.LuaCs.Hook.Call("writeClientList", c, outmsg); - outmsg.Write((byte)connectedClients.Count); + outmsg.WriteByte((byte)connectedClients.Count); foreach (Client client in connectedClients) { var tempClientData = new TempClient { - ID = client.ID, - SteamID = client.SteamID, - NameID = client.NameID, + SessionId = client.SessionId, + AccountInfo = client.AccountInfo, + NameId = client.NameId, Name = client.Name, - PreferredJob = client.Character?.Info?.Job != null && gameStarted + PreferredJob = client.Character?.Info?.Job != null && GameStarted ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob, PreferredTeam = client.PreferredTeam, - CharacterID = client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID, + CharacterId = client.Character == null || !GameStarted ? (ushort)0 : client.Character.ID, Karma = c.HasPermission(ClientPermissions.ServerLog) ? client.Karma : 100.0f, Muted = client.Muted, InGame = client.InGame, HasPermissions = client.Permissions != ClientPermissions.None, IsOwner = client.Connection == OwnerConnection, - AllowKicking = client.Connection != OwnerConnection && - !client.HasPermission(ClientPermissions.Ban) && - !client.HasPermission(ClientPermissions.Kick) && - !client.HasPermission(ClientPermissions.Unban), IsDownloading = FileSender.ActiveTransfers.Any(t => t.Connection == client.Connection) }; @@ -1853,8 +1861,8 @@ namespace Barotrauma.Networking { tempClientData = result.Value; } - - outmsg.Write(tempClientData); + + outmsg.WriteNetSerializableStruct(tempClientData); outmsg.WritePadBits(); } } @@ -1864,27 +1872,32 @@ namespace Barotrauma.Networking bool isInitialUpdate = false; IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); + outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); - outmsg.Write((byte)ServerNetObject.SYNC_IDS); + outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); int settingsBytes = outmsg.LengthBytes; int initialUpdateBytes = 0; + if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None) + { + GameMain.NetLobbyScreen.LastUpdateID++; + } + IWriteMessage settingsBuf = null; if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) { - outmsg.Write(true); + outmsg.WriteBoolean(true); outmsg.WritePadBits(); - outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); settingsBuf = new ReadWriteMessage(); - serverSettings.ServerWrite(settingsBuf, c); - outmsg.Write((UInt16)settingsBuf.LengthBytes); - outmsg.Write(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); + ServerSettings.ServerWrite(settingsBuf, c); + outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); + outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); - outmsg.Write(c.LastRecvLobbyUpdate < 1); + outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); if (c.LastRecvLobbyUpdate < 1) { isInitialUpdate = true; @@ -1892,42 +1905,42 @@ namespace Barotrauma.Networking ClientWriteInitial(c, outmsg); initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; } - outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); - outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.Write(IsUsingRespawnShuttle()); - var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? - respawnManager.RespawnShuttle.Info : + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); + outmsg.WriteBoolean(IsUsingRespawnShuttle()); + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; - outmsg.Write(selectedShuttle.Name); - outmsg.Write(selectedShuttle.MD5Hash.ToString()); + outmsg.WriteString(selectedShuttle.Name); + outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); - outmsg.Write(serverSettings.AllowSubVoting); - outmsg.Write(serverSettings.AllowModeVoting); + outmsg.WriteBoolean(ServerSettings.AllowSubVoting); + outmsg.WriteBoolean(ServerSettings.AllowModeVoting); - outmsg.Write(serverSettings.VoiceChatEnabled); + outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); - outmsg.Write(serverSettings.AllowSpectating); + outmsg.WriteBoolean(ServerSettings.AllowSpectating); - outmsg.WriteRangedInteger((int)serverSettings.TraitorsEnabled, 0, 2); + outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); - outmsg.Write((byte)GameMain.NetLobbyScreen.SelectedModeIndex); - outmsg.Write(GameMain.NetLobbyScreen.LevelSeed); - outmsg.Write(serverSettings.SelectedLevelDifficulty); + outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); + outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); + outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); - outmsg.Write((byte)serverSettings.BotCount); - outmsg.Write(serverSettings.BotSpawnMode == BotSpawnMode.Fill); + outmsg.WriteByte((byte)ServerSettings.BotCount); + outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); - outmsg.Write(serverSettings.AutoRestart); - if (serverSettings.AutoRestart) + outmsg.WriteBoolean(ServerSettings.AutoRestart); + if (ServerSettings.AutoRestart) { - outmsg.Write(autoRestartTimerRunning ? serverSettings.AutoRestartTimer : 0.0f); + outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); } } else { - outmsg.Write(false); + outmsg.WriteBoolean(false); outmsg.WritePadBits(); } settingsBytes = outmsg.LengthBytes - settingsBytes; @@ -1937,18 +1950,18 @@ namespace Barotrauma.Networking if (outmsg.LengthBytes < MsgConstants.MTU - 500 && campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { - outmsg.Write(true); + outmsg.WriteBoolean(true); outmsg.WritePadBits(); campaign.ServerWrite(outmsg, c); } else { - outmsg.Write(false); + outmsg.WriteBoolean(false); outmsg.WritePadBits(); } campaignBytes = outmsg.LengthBytes - campaignBytes; - outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server int clientListBytes = outmsg.LengthBytes; if (outmsg.LengthBytes < MsgConstants.MTU - 500) @@ -1961,7 +1974,7 @@ namespace Barotrauma.Networking WriteChatMessages(outmsg, c); chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; - outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); bool messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; if (messageTooLarge && !isInitialUpdate) @@ -2030,13 +2043,13 @@ namespace Barotrauma.Networking public bool StartGame() { - if (initiatedStartGame || gameStarted) { return false; } + if (initiatedStartGame || GameStarted) { return false; } Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; SubmarineInfo selectedSub; - if (serverSettings.AllowSubVoting) + if (ServerSettings.AllowSubVoting) { selectedSub = Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } @@ -2075,21 +2088,21 @@ namespace Barotrauma.Networking if (connectedClients.Any()) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME); + msg.WriteByte((byte)ServerPacketHeader.QUERY_STARTGAME); - msg.Write(selectedSub.Name); - msg.Write(selectedSub.MD5Hash.StringRepresentation); + msg.WriteString(selectedSub.Name); + msg.WriteString(selectedSub.MD5Hash.StringRepresentation); - msg.Write(IsUsingRespawnShuttle()); - msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.StringRepresentation); + msg.WriteBoolean(IsUsingRespawnShuttle()); + msg.WriteString(selectedShuttle.Name); + msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation); var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); - msg.Write(campaign == null ? (UInt16)0 : campaign.LastSaveID); + msg.WriteByte(campaign == null ? (byte)0 : campaign.CampaignID); + msg.WriteUInt16(campaign == null ? (UInt16)0 : campaign.LastSaveID); foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) { - msg.Write(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag)); + msg.WriteUInt16(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag)); } connectedClients.ForEach(c => c.ReadyToStart = false); @@ -2103,7 +2116,7 @@ namespace Barotrauma.Networking float waitForResponseTimer = 5.0f; while (connectedClients.Any(c => !c.ReadyToStart) && waitForResponseTimer > 0.0f) { - waitForResponseTimer -= CoroutineManager.UnscaledDeltaTime; + waitForResponseTimer -= CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } @@ -2112,7 +2125,7 @@ namespace Barotrauma.Networking float waitForTransfersTimer = 20.0f; while (FileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { - waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; + waitForTransfersTimer -= CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } } @@ -2161,7 +2174,7 @@ namespace Barotrauma.Networking List playingClients = new List(connectedClients); - if (serverSettings.AllowSpectating) + if (ServerSettings.AllowSpectating) { playingClients.RemoveAll(c => c.SpectateOnly); } @@ -2207,7 +2220,7 @@ namespace Barotrauma.Networking else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); + GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, ServerSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); @@ -2229,9 +2242,9 @@ namespace Barotrauma.Networking bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - if (serverSettings.AllowRespawn && missionAllowRespawn) + if (ServerSettings.AllowRespawn && missionAllowRespawn) { - respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); + RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); } if (campaign != null) { @@ -2271,7 +2284,7 @@ namespace Barotrauma.Networking //find the clients in this team List teamClients = teamCount == 1 ? new List(playingClients) : playingClients.FindAll(c => c.TeamID == teamID); - if (serverSettings.AllowSpectating) + if (ServerSettings.AllowSpectating) { teamClients.RemoveAll(c => c.SpectateOnly); } @@ -2309,7 +2322,7 @@ namespace Barotrauma.Networking // do not load new bots if we already have them if (crewManager == null || !crewManager.HasBots) { - int botsToSpawn = serverSettings.BotSpawnMode == BotSpawnMode.Fill ? serverSettings.BotCount - characterInfos.Count : serverSettings.BotCount; + int botsToSpawn = ServerSettings.BotSpawnMode == BotSpawnMode.Fill ? ServerSettings.BotCount - characterInfos.Count : ServerSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) { var botInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName) @@ -2404,7 +2417,7 @@ namespace Barotrauma.Networking mpCampaign.ClearSavedExperiencePoints(teamClients[i]); } - spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; + spawnedCharacter.OwnerClientAddress = teamClients[i].Connection.Endpoint.Address; spawnedCharacter.OwnerClientName = teamClients[i].Name; } @@ -2447,7 +2460,7 @@ namespace Barotrauma.Networking { if (sub == null) { continue; } List spawnList = new List(); - foreach (KeyValuePair kvp in serverSettings.ExtraCargo) + foreach (KeyValuePair kvp in ServerSettings.ExtraCargo) { spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } @@ -2456,8 +2469,8 @@ namespace Barotrauma.Networking } TraitorManager = null; - if (serverSettings.TraitorsEnabled == YesNoMaybe.Yes || - (serverSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) + if (ServerSettings.TraitorsEnabled == YesNoMaybe.Yes || + (ServerSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) { if (!(GameMain.GameSession?.GameMode is CampaignMode)) { @@ -2473,13 +2486,13 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - Voting?.ResetVotes(GameMain.Server.ConnectedClients); + Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); - gameStarted = true; + GameStarted = true; initiatedStartGame = false; GameMain.ResetFrameTime(); @@ -2507,50 +2520,50 @@ namespace Barotrauma.Networking MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.STARTGAME); - msg.Write(seed); - msg.Write(gameSession.GameMode.Preset.Identifier); + msg.WriteByte((byte)ServerPacketHeader.STARTGAME); + msg.WriteInt32(seed); + msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); - msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); - msg.Write(serverSettings.AllowDisguises); - msg.Write(serverSettings.AllowRewiring); - msg.Write(serverSettings.AllowFriendlyFire); - msg.Write(serverSettings.LockAllDefaultWires); - msg.Write(serverSettings.AllowRagdollButton); - msg.Write(serverSettings.AllowLinkingWifiToChat); - msg.Write(serverSettings.MaximumMoneyTransferRequest); - msg.Write(IsUsingRespawnShuttle()); - msg.Write((byte)serverSettings.LosMode); - msg.Write(includesFinalize); msg.WritePadBits(); + msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); + msg.WriteBoolean(ServerSettings.AllowDisguises); + msg.WriteBoolean(ServerSettings.AllowRewiring); + msg.WriteBoolean(ServerSettings.AllowFriendlyFire); + msg.WriteBoolean(ServerSettings.LockAllDefaultWires); + msg.WriteBoolean(ServerSettings.AllowRagdollButton); + msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); + msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); + msg.WriteBoolean(IsUsingRespawnShuttle()); + msg.WriteByte((byte)ServerSettings.LosMode); + msg.WriteBoolean(includesFinalize); msg.WritePadBits(); - serverSettings.WriteMonsterEnabled(msg); + ServerSettings.WriteMonsterEnabled(msg); if (campaign == null) { - msg.Write(levelSeed); - msg.Write(serverSettings.SelectedLevelDifficulty); - msg.Write(gameSession.SubmarineInfo.Name); - msg.Write(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); - var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? - respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; - msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.StringRepresentation); - msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); + msg.WriteString(levelSeed); + msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); + msg.WriteString(gameSession.SubmarineInfo.Name); + msg.WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + msg.WriteString(selectedShuttle.Name); + msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation); + msg.WriteByte((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { - msg.Write(mission.Prefab.UintIdentifier); + msg.WriteUInt32(mission.Prefab.UintIdentifier); } } else { int nextLocationIndex = campaign.Map.Locations.FindIndex(l => l.LevelData == campaign.NextLevel); int nextConnectionIndex = campaign.Map.Connections.FindIndex(c => c.LevelData == campaign.NextLevel); - msg.Write(campaign.CampaignID); - msg.Write(campaign.LastSaveID); - msg.Write(nextLocationIndex); - msg.Write(nextConnectionIndex); - msg.Write(campaign.Map.SelectedLocationIndex); - msg.Write(campaign.MirrorLevel); + msg.WriteByte(campaign.CampaignID); + msg.WriteUInt16(campaign.LastSaveID); + msg.WriteInt32(nextLocationIndex); + msg.WriteInt32(nextConnectionIndex); + msg.WriteInt32(campaign.Map.SelectedLocationIndex); + msg.WriteBoolean(campaign.MirrorLevel); } if (includesFinalize) @@ -2563,13 +2576,13 @@ namespace Barotrauma.Networking private bool IsUsingRespawnShuttle() { - return serverSettings.UseRespawnShuttle || (gameStarted && respawnManager != null && respawnManager.UsingShuttle); + return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); } private void SendRoundStartFinalize(Client client) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.STARTGAMEFINALIZE); + msg.WriteByte((byte)ServerPacketHeader.STARTGAMEFINALIZE); WriteRoundStartFinalize(msg, client); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -2578,32 +2591,32 @@ namespace Barotrauma.Networking { //tell the client what content files they should preload var contentToPreload = GameMain.GameSession.EventManager.GetFilesToPreload(); - msg.Write((ushort)contentToPreload.Count()); + msg.WriteUInt16((ushort)contentToPreload.Count()); foreach (ContentFile contentFile in contentToPreload) { - msg.Write(contentFile.Path.Value); + msg.WriteString(contentFile.Path.Value); } - msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); - msg.Write((byte)GameMain.GameSession.Missions.Count()); + msg.WriteInt32(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); + msg.WriteByte((byte)GameMain.GameSession.Missions.Count()); foreach (Mission mission in GameMain.GameSession.Missions) { - msg.Write(mission.Prefab.Identifier); + msg.WriteIdentifier(mission.Prefab.Identifier); } foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType().OrderBy(s => s)) { - msg.Write(GameMain.GameSession.Level.EqualityCheckValues[stage]); + msg.WriteInt32(GameMain.GameSession.Level.EqualityCheckValues[stage]); } foreach (Mission mission in GameMain.GameSession.Missions) { mission.ServerWriteInitial(msg, client); } - msg.Write(GameMain.GameSession.CrewManager != null); + msg.WriteBoolean(GameMain.GameSession.CrewManager != null); GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false) { - if (!gameStarted) + if (!GameStarted) { return; } @@ -2635,14 +2648,14 @@ namespace Barotrauma.Networking endRoundTimer = 0.0f; - if (serverSettings.AutoRestart) + if (ServerSettings.AutoRestart) { - serverSettings.AutoRestartTimer = serverSettings.AutoRestartInterval; + ServerSettings.AutoRestartTimer = ServerSettings.AutoRestartInterval; //send a netlobby update to get the clients' autorestart timers up to date GameMain.NetLobbyScreen.LastUpdateID++; } - if (serverSettings.SaveServerLogs) { serverSettings.ServerLog.Save(); } + if (ServerSettings.SaveServerLogs) { ServerSettings.ServerLog.Save(); } GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; @@ -2656,28 +2669,24 @@ namespace Barotrauma.Networking KarmaManager.OnRoundEnded(); -#if DEBUG - messageCount.Clear(); -#endif - - respawnManager = null; - gameStarted = false; + RespawnManager = null; + GameStarted = false; if (connectedClients.Count > 0) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.ENDGAME); - msg.Write((byte)transitionType); - msg.Write(wasSaved); - msg.Write(endMessage); - msg.Write((byte)missions.Count); + msg.WriteByte((byte)ServerPacketHeader.ENDGAME); + msg.WriteByte((byte)transitionType); + msg.WriteBoolean(wasSaved); + msg.WriteString(endMessage); + msg.WriteByte((byte)missions.Count); foreach (Mission mission in missions) { - msg.Write(mission.Completed); + msg.WriteBoolean(mission.Completed); } - msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); + msg.WriteByte(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); - msg.Write((byte)traitorResults.Count); + msg.WriteByte((byte)traitorResults.Count); foreach (var traitorResult in traitorResults) { traitorResult.ServerWrite(msg); @@ -2726,9 +2735,15 @@ namespace Barotrauma.Networking Identifier newJob = inc.ReadIdentifier(); CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); - if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } - - c.NameID = nameId; + if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } + if (!newJob.IsEmpty) + { + if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) + { + newJob = Identifier.Empty; + } + } + c.NameId = nameId; if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } var result = GameMain.LuaCs.Hook.Call("tryChangeClientName", c, newName, newJob, newTeam); @@ -2758,7 +2773,6 @@ namespace Barotrauma.Networking { string oldName = c.Name; c.Name = newName; - c.Connection.Name = newName; SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); return true; } @@ -2775,7 +2789,7 @@ namespace Barotrauma.Networking if (c.Connection != OwnerConnection) { - if (!Client.IsValidName(newName, serverSettings)) + if (!Client.IsValidName(newName, ServerSettings)) { SendDirectChatMessage($"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; @@ -2822,7 +2836,7 @@ namespace Barotrauma.Networking public void KickClient(Client client, string reason, bool resetKarma = false) { - if (client == null || client.Connection == OwnerConnection) return; + if (client == null || client.Connection == OwnerConnection) { return; } if (resetKarma) { @@ -2834,12 +2848,10 @@ namespace Barotrauma.Networking client.Karma = Math.Max(client.Karma, 50.0f); } - string msg = DisconnectReason.Kicked.ToString(); - string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; - DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); + DisconnectClient(client, PeerDisconnectPacket.Kicked(reason)); } - public override void BanPlayer(string playerName, string reason, bool range = false, TimeSpan? duration = null) + public override void BanPlayer(string playerName, string reason, TimeSpan? duration = null) { Client client = connectedClients.Find(c => c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || @@ -2851,10 +2863,10 @@ namespace Barotrauma.Networking return; } - BanClient(client, reason, range, duration); + BanClient(client, reason, duration); } - public void BanClient(Client client, string reason, bool range = false, TimeSpan? duration = null) + public void BanClient(Client client, string reason, TimeSpan? duration = null) { if (client == null || client.Connection == OwnerConnection) { return; } @@ -2866,48 +2878,37 @@ 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, PlayerConnectionChangeType.Banned); + DisconnectClient(client, PeerDisconnectPacket.Banned(reason)); - if (client.Connection is LidgrenConnection lidgrenConn && (client.SteamID == 0 || range)) + if (client.AccountInfo.AccountId.TryUnwrap(out var accountId)) { - string ip = ""; - ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? - lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : - lidgrenConn.IPEndPoint.Address.ToString(); - if (range) { ip = BanList.ToRange(ip); } - serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); + ServerSettings.BanList.BanPlayer(client.Name, accountId, reason, duration); } - if (client.SteamID > 0) + else { - serverSettings.BanList.BanPlayer(client.Name, client.SteamID, reason, duration); + ServerSettings.BanList.BanPlayer(client.Name, client.Connection.Endpoint, reason, duration); } - if (client.OwnerSteamID > 0) + foreach (var relatedId in client.AccountInfo.OtherMatchingIds) { - serverSettings.BanList.BanPlayer(client.Name, client.OwnerSteamID, reason, duration); + ServerSettings.BanList.BanPlayer(client.Name, relatedId, reason, duration); } } - public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, bool range = false, TimeSpan? duration = null) + public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, TimeSpan? duration = null) { if (previousPlayer == null) { return; } //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); - if (!string.IsNullOrEmpty(previousPlayer.EndPoint) && (previousPlayer.SteamID == 0 || range)) + ServerSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.Address, reason, duration); + if (previousPlayer.AccountInfo.AccountId.TryUnwrap(out var accountId)) { - string ip = previousPlayer.EndPoint; - if (range) { ip = BanList.ToRange(ip); } - serverSettings.BanList.BanPlayer(previousPlayer.Name, ip, reason, duration); + ServerSettings.BanList.BanPlayer(previousPlayer.Name, accountId, reason, duration); } - if (previousPlayer.SteamID > 0) + foreach (var relatedId in previousPlayer.AccountInfo.OtherMatchingIds) { - serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.SteamID, reason, duration); - } - if (previousPlayer.OwnerSteamID > 0) - { - serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.OwnerSteamID, reason, duration); + ServerSettings.BanList.BanPlayer(previousPlayer.Name, relatedId, reason, duration); } string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}"; @@ -2918,39 +2919,34 @@ namespace Barotrauma.Networking SendChatMessage(msg, ChatMessageType.Server, changeType: PlayerConnectionChangeType.Banned); } - public override void UnbanPlayer(string playerName, string playerEndPoint) + public override void UnbanPlayer(string playerName) { - if (!string.IsNullOrEmpty(playerEndPoint)) - { - serverSettings.BanList.UnbanEndPoint(playerEndPoint); - } - else if (!string.IsNullOrEmpty(playerName)) - { - serverSettings.BanList.UnbanPlayer(playerName); - } + BannedPlayer bannedPlayer + = ServerSettings.BanList.BannedPlayers.FirstOrDefault(bp => bp.Name == playerName); + if (bannedPlayer is null) { return; } + ServerSettings.BanList.UnbanPlayer(bannedPlayer.AddressOrAccountId); } - public void DisconnectClient(NetworkConnection senderConnection, string msg = "", string targetmsg = "") + public override void UnbanPlayer(Endpoint endpoint) + { + ServerSettings.BanList.UnbanPlayer(endpoint); + } + + public void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket) { - if (senderConnection == OwnerConnection) - { - DebugConsole.NewMessage("Owner disconnected: closing the server...", Color.Yellow); - Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); - GameMain.ShouldRun = false; - } Client client = connectedClients.Find(x => x.Connection == senderConnection); - if (client == null) return; + if (client == null) { return; } - DisconnectClient(client, msg, targetmsg, string.Empty, PlayerConnectionChangeType.Disconnected); + DisconnectClient(client, peerDisconnectPacket); } - public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "", PlayerConnectionChangeType changeType = PlayerConnectionChangeType.Disconnected) + public void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectPacket) { if (client == null) return; GameMain.LuaCs.Hook.Call("client.disconnected", client); - if (gameStarted && client.Character != null) + if (client.Character != null) { client.Character.ClientDisconnected = true; client.Character.ClearInputs(); @@ -2961,15 +2957,7 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={ClientLogName(client)}"; } - if (string.IsNullOrWhiteSpace(targetmsg)) { targetmsg = "ServerMessage.YouLeftServer"; } - if (!string.IsNullOrWhiteSpace(reason)) - { - msg += $"/ /ServerMessage.Reason/: /{reason}"; - targetmsg += $"/\n/ServerMessage.Reason/: /{reason}"; - } - - if (client.SteamID != 0) { SteamManager.StopAuthSession(client.SteamID); } + if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -2986,19 +2974,19 @@ namespace Barotrauma.Networking if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); } } - serverPeer.Disconnect(client.Connection, targetmsg); client.Dispose(); connectedClients.Remove(client); + serverPeer.Disconnect(client.Connection, peerDisconnectPacket); KarmaManager.OnClientDisconnected(client); UpdateVoteStatus(); - SendChatMessage(msg, ChatMessageType.Server, changeType: changeType); + SendChatMessage(peerDisconnectPacket.ChatMessage(client).Value, ChatMessageType.Server, changeType: peerDisconnectPacket.ConnectionChangeType); UpdateCrewFrame(); - serverSettings.ServerDetailsChanged = true; + ServerSettings.ServerDetailsChanged = true; refreshMasterTimer = DateTime.Now; } @@ -3113,7 +3101,7 @@ namespace Barotrauma.Networking message = tempStr; } - if (gameStarted) + if (GameStarted) { if (senderClient == null) { @@ -3294,9 +3282,9 @@ namespace Barotrauma.Networking public void SendCancelTransferMsg(FileSender.FileTransferOut transfer) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.FILE_TRANSFER); - msg.Write((byte)FileTransferMessageType.Cancel); - msg.Write((byte)transfer.ID); + msg.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); + msg.WriteByte((byte)FileTransferMessageType.Cancel); + msg.WriteByte((byte)transfer.ID); serverPeer.Send(msg, transfer.Connection, DeliveryMethod.ReliableOrdered); } @@ -3318,11 +3306,11 @@ namespace Barotrauma.Networking int no = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 1); int max = eligibleClients.Count(); // Required ratio cannot be met - if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) + if (no / (float)max > 1f - ServerSettings.VoteRequiredRatio) { Voting.ActiveVote.Finish(Voting, passed: false); } - else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + else if (yes / (float)max >= ServerSettings.VoteRequiredRatio) { Voting.ActiveVote.Finish(Voting, passed: true); } @@ -3331,7 +3319,8 @@ namespace Barotrauma.Networking Client.UpdateKickVotes(connectedClients); - int minimumKickVotes = Math.Max(1, (int)(connectedClients.Count * serverSettings.KickVoteRequiredRatio)); + var kickVoteEligibleClients = connectedClients.Where(c => (DateTime.Now - c.JoinTime).TotalSeconds > ServerSettings.DisallowKickVoteTime); + float minimumKickVotes = Math.Max(2.0f, kickVoteEligibleClients.Count() * ServerSettings.KickVoteRequiredRatio); var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && !c.HasPermission(ClientPermissions.Kick) && @@ -3340,16 +3329,10 @@ namespace Barotrauma.Networking c.KickVoteCount >= minimumKickVotes); foreach (Client c in clientsToKick) { - var previousPlayer = previousPlayers.Find(p => p.MatchesClient(c)); - if (previousPlayer != null) - { - //reset the client's kick votes (they can rejoin after their ban expires) - previousPlayer.KickVoters.Clear(); - } - - 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)); + //reset the client's kick votes (they can rejoin after their ban expires) + c.ResetVotes(resetKickVotes: true); + previousPlayers.Where(p => p.MatchesClient(c)).ForEach(p => p.KickVoters.Clear()); + BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(ServerSettings.AutoBanTime)); } //GameMain.NetLobbyScreen.LastUpdateID++; @@ -3358,8 +3341,8 @@ namespace Barotrauma.Networking int endVoteCount = ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); int endVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned); - if (serverSettings.AllowEndVoting && endVoteMax > 0 && - ((float)endVoteCount / (float)endVoteMax) >= serverSettings.EndVoteRequiredRatio) + if (ServerSettings.AllowEndVoting && endVoteMax > 0 && + ((float)endVoteCount / (float)endVoteMax) >= ServerSettings.EndVoteRequiredRatio) { Log("Ending round by votes (" + endVoteCount + "/" + (endVoteMax - endVoteCount) + ")", ServerLog.MessageType.ServerMessage); EndGame(wasSaved: false); @@ -3371,10 +3354,10 @@ namespace Barotrauma.Networking if (!recipients.Any()) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); - msg.Write((byte)ServerNetObject.VOTE); + msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); + msg.WriteByte((byte)ServerNetObject.VOTE); Voting.ServerWrite(msg); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); foreach (var c in recipients) { @@ -3415,26 +3398,26 @@ namespace Barotrauma.Networking public void UpdateClientPermissions(Client client) { - if (client.SteamID > 0) + if (client.AccountId.TryUnwrap(out var accountId)) { - serverSettings.ClientPermissions.RemoveAll(cp => cp.SteamID == client.SteamID); + ServerSettings.ClientPermissions.RemoveAll(scp => scp.AddressOrAccountId == accountId); if (client.Permissions != ClientPermissions.None) { - serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + ServerSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, - client.SteamID, + accountId, client.Permissions, client.PermittedConsoleCommands)); } } else { - serverSettings.ClientPermissions.RemoveAll(cp => client.EndpointMatches(cp.EndPoint)); + ServerSettings.ClientPermissions.RemoveAll(scp => client.Connection.Endpoint.Address == scp.AddressOrAccountId); if (client.Permissions != ClientPermissions.None) { - serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + ServerSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, - client.Connection.EndPointString, + client.Connection.Endpoint.Address, client.Permissions, client.PermittedConsoleCommands)); } @@ -3444,13 +3427,13 @@ namespace Barotrauma.Networking { CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(recipient, client)); } - serverSettings.SaveClientPermissions(); + ServerSettings.SaveClientPermissions(); } private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) { DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); - while (recipient.LastRecvClientListUpdate < LastClientListUpdateID) + while (NetIdUtils.IdMoreRecent(LastClientListUpdateID, recipient.LastRecvClientListUpdate)) { if (DateTime.Now > timeOut || GameMain.Server == null || !connectedClients.Contains(recipient)) { @@ -3468,7 +3451,7 @@ namespace Barotrauma.Networking if (recipient?.Connection == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.PERMISSIONS); + msg.WriteByte((byte)ServerPacketHeader.PERMISSIONS); client.WritePermissions(msg); serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable); } @@ -3503,9 +3486,9 @@ namespace Barotrauma.Networking client.GivenAchievements.Add(achievementIdentifier); IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); - msg.Write(achievementIdentifier); - msg.Write(0); + msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT); + msg.WriteIdentifier(achievementIdentifier); + msg.WriteInt32(0); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3515,9 +3498,9 @@ namespace Barotrauma.Networking if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); - msg.Write(achievementIdentifier); - msg.Write(amount); + msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT); + msg.WriteIdentifier(achievementIdentifier); + msg.WriteInt32(amount); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3526,10 +3509,10 @@ namespace Barotrauma.Networking { if (client == null) { return; } var msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); - msg.Write((byte)messageType); - msg.Write(missionIdentifier); - msg.Write(message); + msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); + msg.WriteByte((byte)messageType); + msg.WriteIdentifier(missionIdentifier); + msg.WriteString(message); serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } @@ -3538,8 +3521,8 @@ namespace Barotrauma.Networking if (!connectedClients.Any()) { return; } IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.CHEATS_ENABLED); - msg.Write(DebugConsole.CheatsEnabled); + msg.WriteByte((byte)ServerPacketHeader.CHEATS_ENABLED); + msg.WriteBoolean(DebugConsole.CheatsEnabled); msg.WritePadBits(); foreach (Client c in connectedClients) @@ -3556,7 +3539,7 @@ namespace Barotrauma.Networking if (client.Character != null) { client.Character.IsRemotePlayer = false; - client.Character.OwnerClientEndPoint = null; + client.Character.OwnerClientAddress = null; client.Character.OwnerClientName = null; } @@ -3583,7 +3566,7 @@ namespace Barotrauma.Networking newCharacter.Info.Character = newCharacter; } - newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; + newCharacter.OwnerClientAddress = client.Connection.Endpoint.Address; newCharacter.OwnerClientName = client.Name; newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; @@ -3594,7 +3577,7 @@ namespace Barotrauma.Networking private void UpdateCharacterInfo(IReadMessage message, Client sender) { - sender.SpectateOnly = message.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); if (sender.SpectateOnly) { return; @@ -3634,14 +3617,14 @@ namespace Barotrauma.Networking List jobPreferences = new List(); int count = message.ReadByte(); - // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) { string jobIdentifier = message.ReadString(); int variant = message.ReadByte(); - if (JobPrefab.Prefabs.ContainsKey(jobIdentifier)) + if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab)) { - jobPreferences.Add(new JobVariant(JobPrefab.Prefabs[jobIdentifier], variant)); + if (jobPrefab.HiddenJob) { continue; } + jobPreferences.Add(new JobVariant(jobPrefab, variant)); } } @@ -3943,9 +3926,9 @@ namespace Barotrauma.Networking foreach (var client in connectedClients) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.MISSION); + msg.WriteByte((byte)ServerPacketHeader.MISSION); int missionIndex = GameMain.GameSession.GetMissionIndex(mission); - msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); + msg.WriteByte((byte)(missionIndex == -1 ? 255: missionIndex)); mission?.ServerWrite(msg); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3976,51 +3959,40 @@ namespace Barotrauma.Networking } } - public Tuple FindPreviousClientData(Client client) - { - var player = previousPlayers.Find(p => p.MatchesClient(client)); - if (player != null) - { - return Tuple.Create(player.SteamID, player.EndPoint); - } - return null; - } - - public override void Disconnect() + public void Quit() { if (started) { started = false; - serverSettings.BanList.Save(); + ServerSettings.BanList.Save(); - if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } - if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } + if (GameMain.NetLobbyScreen.SelectedSub != null) { ServerSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } + if (GameMain.NetLobbyScreen.SelectedShuttle != null) { ServerSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } - serverSettings.SaveSettings(); + ServerSettings.SaveSettings(); ModSender.Dispose(); - if (serverSettings.SaveServerLogs) + if (ServerSettings.SaveServerLogs) { Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); - serverSettings.ServerLog.Save(); + ServerSettings.ServerLog.Save(); } GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown"); - serverPeer?.Close(DisconnectReason.ServerShutdown.ToString()); + serverPeer?.Close(); SteamManager.CloseServer(); } } } - partial class PreviousPlayer + class PreviousPlayer { public string Name; - public string EndPoint; - public UInt64 SteamID; - public UInt64 OwnerSteamID; + public Address Address; + public AccountInfo AccountInfo; public float Karma; public int KarmaKickCount; public readonly List KickVoters = new List(); @@ -4028,15 +4000,14 @@ namespace Barotrauma.Networking public PreviousPlayer(Client c) { Name = c.Name; - EndPoint = c.Connection?.EndPointString ?? ""; - SteamID = c.SteamID; - OwnerSteamID = c.OwnerSteamID; + Address = c.Connection.Endpoint.Address; + AccountInfo = c.AccountInfo; } public bool MatchesClient(Client c) { - if (c.SteamID > 0 && SteamID > 0) { return c.SteamID == SteamID; } - return c.EndpointMatches(EndPoint); + if (c.AccountInfo.AccountId.IsSome() && AccountInfo.AccountId.IsSome()) { return c.AccountInfo.AccountId == AccountInfo.AccountId; } + return c.AddressMatches(Address); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e86a858f5..e732ce117 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -188,9 +188,9 @@ namespace Barotrauma } } - if (client.Character?.Info?.Job.Prefab.Identifier == "captain" && client.Character.SelectedConstruction != null) + if (client.Character?.Info?.Job.Prefab.Identifier == "captain" && client.Character.SelectedItem != null) { - if (client.Character.SelectedConstruction.GetComponent() != null) + if (client.Character.SelectedItem.GetComponent() != null) { AdjustKarma(client.Character, SteerSubKarmaIncrease * deltaTime, "Steering the sub"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index e587e882e..abcbbf42c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Networking " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error); - server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncOldEvent + "/ServerMessage.ExcessiveDesyncOldEvent"); + server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent)); } ); } @@ -260,7 +260,7 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red); GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); - server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent"); + server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncRemovedEvent)); }); } } @@ -269,7 +269,7 @@ namespace Barotrauma.Networking foreach (Client timedOutClient in timedOutClients) { 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"); + GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout)); } bufferedEvents.RemoveAll(b => b.IsProcessed); @@ -344,15 +344,15 @@ namespace Barotrauma.Networking if (client.NeedsMidRoundSync) { - msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL); - msg.Write(client.UnreceivedEntityEventCount); - msg.Write(client.FirstNewEventID); + msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT_INITIAL); + msg.WriteUInt16(client.UnreceivedEntityEventCount); + msg.WriteUInt16(client.FirstNewEventID); Write(msg, eventsToSync, out sentEvents, client); } else { - msg.Write((byte)ServerNetObject.ENTITY_EVENT); + msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT); Write(msg, eventsToSync, out sentEvents, client); } @@ -499,7 +499,7 @@ namespace Barotrauma.Networking ReadWriteMessage buffer = new ReadWriteMessage(); byte[] temp = msg.ReadBytes(msgLength - 2); - buffer.Write(temp, 0, msgLength - 2); + buffer.WriteBytes(temp, 0, msgLength - 2); buffer.BitPosition = 0; BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index 1aad3a99d..21d3f9ae8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Steam; +using System; namespace Barotrauma.Networking { @@ -6,21 +7,21 @@ namespace Barotrauma.Networking { public override void ServerWrite(IWriteMessage msg, Client c) { - msg.Write((byte)ServerNetObject.CHAT_MESSAGE); - msg.Write(NetStateID); + msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); - msg.Write(SenderName); - msg.Write(SenderClient != null); + msg.WriteString(SenderName); + msg.WriteBoolean(SenderClient != null); if (SenderClient != null) { - msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + msg.WriteString(SenderClient.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : SenderClient.SessionId.ToString()); } - msg.Write(Sender != null && c.InGame); + msg.WriteBoolean(Sender != null && c.InGame); if (Sender != null && c.InGame) { - msg.Write(Sender.ID); - } - msg.Write(false); //text color (no custom text colors for order messages) + msg.WriteUInt16(Sender.ID); + } + msg.WriteBoolean(false); //text color (no custom text colors for order messages) msg.WritePadBits(); WriteOrder(msg); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 634c1db54..71a305d05 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -1,24 +1,45 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Net; using System.Linq; +using Barotrauma.Steam; using Lidgren.Network; namespace Barotrauma.Networking { - class LidgrenServerPeer : ServerPeer + internal sealed class LidgrenServerPeer : ServerPeer { - private NetPeerConfiguration netPeerConfiguration; - private NetServer netServer; + private readonly NetPeerConfiguration netPeerConfiguration; + private NetServer? netServer; private readonly List incomingLidgrenMessages; - public LidgrenServerPeer(int? ownKey, ServerSettings settings) + public LidgrenServerPeer(Option ownKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) { serverSettings = settings; netServer = null; + netPeerConfiguration = new NetPeerConfiguration("barotrauma") + { + AcceptIncomingConnections = true, + AutoExpandMTU = false, + MaximumConnections = NetConfig.MaxPlayers * 2, + EnableUPnP = serverSettings.EnableUPnP, + Port = serverSettings.Port + }; + + netPeerConfiguration.DisableMessageType( + NetIncomingMessageType.DebugMessage + | NetIncomingMessageType.WarningMessage + | NetIncomingMessageType.Receipt + | NetIncomingMessageType.ErrorMessage + | NetIncomingMessageType.Error + | NetIncomingMessageType.UnconnectedData); + + netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); + connectedClients = new List(); pendingClients = new List(); @@ -31,25 +52,7 @@ namespace Barotrauma.Networking { if (netServer != null) { return; } - var address = serverSettings.ListenIPAddress; - if (address == IPAddress.Any) address = IPAddress.IPv6Any; - - netPeerConfiguration = new NetPeerConfiguration("barotrauma") - { - AcceptIncomingConnections = true, - AutoExpandMTU = false, - MaximumConnections = NetConfig.MaxPlayers * 2, - EnableUPnP = serverSettings.EnableUPnP, - Port = serverSettings.Port, - LocalAddress = address - }; - - netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | - NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | - NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error | - NetIncomingMessageType.UnconnectedData); - - netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); + incomingLidgrenMessages.Clear(); netServer = new NetServer(netPeerConfiguration); @@ -65,21 +68,21 @@ namespace Barotrauma.Networking } } - public override void Close(string msg = null) + public override void Close() { if (netServer == null) { return; } for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); + RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } - netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString()); + netServer.Shutdown(PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown).ToLidgrenStringRepresentation()); pendingClients.Clear(); connectedClients.Clear(); @@ -88,21 +91,17 @@ namespace Barotrauma.Networking Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange; - OnShutdown?.Invoke(); + callbacks.OnShutdown.Invoke(); } public override void Update(float deltaTime) { - if (netServer == null) { return; } + if (netServer is null) { return; } - if (OnOwnerDetermined != null && OwnerConnection != null) - { - OnOwnerDetermined?.Invoke(OwnerConnection); - OnOwnerDetermined = null; - } + ToolBox.ThrowIfNull(incomingLidgrenMessages); netServer.ReadMessages(incomingLidgrenMessages); - + //process incoming connections first foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType == NetIncomingMessageType.ConnectionApproval)) { @@ -129,7 +128,7 @@ namespace Barotrauma.Networking catch (Exception e) { string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce($"LidgrenServerPeer.Update:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #else @@ -141,7 +140,8 @@ namespace Barotrauma.Networking { PendingClient pendingClient = pendingClients[i]; - var connection = pendingClient.Connection as LidgrenConnection; + LidgrenConnection connection = (LidgrenConnection)pendingClient.Connection; + if (connection.NetConnection.Status == NetConnectionStatus.InitiatedConnect || connection.NetConnection.Status == NetConnectionStatus.ReceivedInitiation || connection.NetConnection.Status == NetConnectionStatus.RespondedAwaitingApproval || @@ -149,6 +149,7 @@ namespace Barotrauma.Networking { continue; } + UpdatePendingClient(pendingClient); if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; } } @@ -158,7 +159,9 @@ namespace Barotrauma.Networking private void InitUPnP() { - if (netServer == null) { return; } + if (netServer is null) { return; } + + ToolBox.ThrowIfNull(netPeerConfiguration); netServer.UPnP.ForwardPort(netPeerConfiguration.Port, "barotrauma"); #if USE_STEAM @@ -193,71 +196,74 @@ namespace Barotrauma.Networking if (!skipDeny && connectedClients.Count >= serverSettings.MaxPlayers) { - inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString()); + inc.SenderConnection.Deny(PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull).ToLidgrenStringRepresentation()); return; } - if (serverSettings.BanList.IsBanned(inc.SenderConnection.RemoteEndPoint.Address, 0, 0, out string banReason)) + if (serverSettings.BanList.IsBanned(new LidgrenEndpoint(inc.SenderConnection.RemoteEndPoint), out string banReason)) { //IP banned: deny immediately - inc.SenderConnection.Deny(DisconnectReason.Banned.ToString() + "/ " + banReason); + inc.SenderConnection.Deny(PeerDisconnectPacket.Banned(banReason).ToLidgrenStringRepresentation()); return; } - PendingClient pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); + PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); - if (pendingClient == null) + if (pendingClient is null) { - pendingClient = new PendingClient(new LidgrenConnection("PENDING", inc.SenderConnection, 0)); + pendingClient = new PendingClient(new LidgrenConnection(inc.SenderConnection)); pendingClients.Add(pendingClient); } inc.SenderConnection.Approve(); } - private void HandleDataMessage(NetIncomingMessage inc) + private void HandleDataMessage(NetIncomingMessage lidgrenMsg) { if (netServer == null) { return; } - PendingClient pendingClient = pendingClients.Find(c => (c.Connection is LidgrenConnection l) && l.NetConnection == inc.SenderConnection); + PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == lidgrenMsg.SenderConnection); - PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); + IReadMessage inc = lidgrenMsg.ToReadMessage(); - if (packetHeader.IsConnectionInitializationStep() && pendingClient != null) + var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); + + if (packetHeader.IsConnectionInitializationStep() && pendingClient != null && initialization.HasValue) { - ReadConnectionInitializationStep(pendingClient, new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); + ReadConnectionInitializationStep(pendingClient, inc, initialization.Value); } else if (!packetHeader.IsConnectionInitializationStep()) { - LidgrenConnection conn = connectedClients.Find(c => (c is LidgrenConnection l) && l.NetConnection == inc.SenderConnection) as LidgrenConnection; - if (conn == null) + if (connectedClients.Find(c => c is LidgrenConnection l && l.NetConnection == lidgrenMsg.SenderConnection) is not LidgrenConnection conn) { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired, "Received data message from unauthenticated client"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired)); } - else if (inc.SenderConnection.Status != NetConnectionStatus.Disconnected && - inc.SenderConnection.Status != NetConnectionStatus.Disconnecting) + else if (lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnected && + lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnecting) { - inc.SenderConnection.Disconnect(DisconnectReason.AuthenticationRequired.ToString() + "/ Received data message from unauthenticated client"); + lidgrenMsg.SenderConnection.Disconnect(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired).ToLidgrenStringRepresentation()); } + return; } + if (pendingClient != null) { pendingClients.Remove(pendingClient); } - if (serverSettings.BanList.IsBanned(conn.IPEndPoint.Address, conn.SteamID, conn.OwnerSteamID, out string banReason)) + + if (serverSettings.BanList.IsBanned(conn.Endpoint, out string banReason) + || (conn.AccountInfo.AccountId.TryUnwrap(out var accountId) && serverSettings.BanList.IsBanned(accountId, out banReason)) + || conn.AccountInfo.OtherMatchingIds.Any(id => serverSettings.BanList.IsBanned(id, out banReason))) { - Disconnect(conn, DisconnectReason.Banned.ToString() + "/ " + banReason); + Disconnect(conn, PeerDisconnectPacket.Banned(banReason)); return; } - UInt16 length = inc.ReadUInt16(); - //DebugConsole.NewMessage(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte + " " + length); - - IReadMessage msg = new ReadOnlyMessage(inc.Data, packetHeader.IsCompressed(), inc.PositionInBytes, length, conn); - OnMessageReceived?.Invoke(conn, msg); + var packet = INetSerializableStruct.Read(inc); + callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); } } - + private void HandleStatusChanged(NetIncomingMessage inc) { if (netServer == null) { return; } @@ -265,30 +271,29 @@ namespace Barotrauma.Networking switch (inc.SenderConnection.Status) { case NetConnectionStatus.Disconnected: - string disconnectMsg; - LidgrenConnection conn = connectedClients.Select(c => c as LidgrenConnection).FirstOrDefault(c => c.NetConnection == inc.SenderConnection); + LidgrenConnection? conn = connectedClients.Cast().FirstOrDefault(c => c.NetConnection == inc.SenderConnection); if (conn != null) { if (conn == OwnerConnection) { DebugConsole.NewMessage("Owner disconnected: closing the server..."); GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); - Close(DisconnectReason.ServerShutdown.ToString() + "/ Owner disconnected"); + Close(); } else { - disconnectMsg = $"ServerMessage.HasDisconnected~[client]={conn.Name}"; - Disconnect(conn, disconnectMsg); + Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } else { - PendingClient pendingClient = pendingClients.Find(c => (c.Connection is LidgrenConnection l) && l.NetConnection == inc.SenderConnection); + PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } + break; } } @@ -298,44 +303,45 @@ namespace Barotrauma.Networking Steamworks.SteamServer.OnValidateAuthTicketResponse += OnAuthChange; } - private void OnAuthChange(Steamworks.SteamId steamID, Steamworks.SteamId ownerID, Steamworks.AuthResponse status) + private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) { if (netServer == null) { return; } - PendingClient pendingClient = pendingClients.Find(c => c.SteamID == steamID); - DebugConsole.Log(steamID + " validation: " + status+", "+(pendingClient!=null)); - - if (pendingClient == null) + PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId); + DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}"); + + if (pendingClient is null) { - if (status != Steamworks.AuthResponse.OK) + if (status == Steamworks.AuthResponse.OK) { return; } + + if (connectedClients.Find(c + => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + is LidgrenConnection connection) { - LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID) as LidgrenConnection; - if (connection != null) - { - Disconnect(connection, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); - } + Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); } + return; } - LidgrenConnection pendingConnection = pendingClient.Connection as LidgrenConnection; - string banReason; - if (serverSettings.BanList.IsBanned(pendingConnection.NetConnection.RemoteEndPoint.Address, steamID, ownerID, out banReason)) + LidgrenConnection pendingConnection = (LidgrenConnection)pendingClient.Connection; + if (serverSettings.BanList.IsBanned(pendingConnection.Endpoint, out string banReason) + || serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason) + || serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); return; } if (status == Steamworks.AuthResponse.OK) { - pendingClient.OwnerSteamID = ownerID; + pendingClient.Connection.SetAccountInfo(new AccountInfo(new SteamId(steamId), new SteamId(ownerId))); pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; pendingClient.UpdateTime = Timing.TotalTime; } else { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam authentication failed: " + status.ToString()); - return; + RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(status)); } } @@ -343,151 +349,144 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - if (!(conn is LidgrenConnection lidgrenConn)) return; - if (!connectedClients.Contains(lidgrenConn)) + if (!connectedClients.Contains(conn)) { - DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + lidgrenConn.IPString); + DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {conn.Endpoint.StringRepresentation}"); return; } - NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - switch (deliveryMethod) - { - case DeliveryMethod.Unreliable: - lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - break; - case DeliveryMethod.Reliable: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; - break; - case DeliveryMethod.ReliableOrdered: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; - break; - } + byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); #if DEBUG + ToolBox.ThrowIfNull(netPeerConfiguration); netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Server.SimulatedDuplicatesChance; netPeerConfiguration.SimulatedMinimumLatency = GameMain.Server.SimulatedMinimumLatency; netPeerConfiguration.SimulatedRandomLatency = GameMain.Server.SimulatedRandomLatency; netPeerConfiguration.SimulatedLoss = GameMain.Server.SimulatedLoss; #endif - NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); - byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); - lidgrenMsg.Write((UInt16)length); - lidgrenMsg.Write(msgData, 0, length); - - NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); - if (result != NetSendResult.Sent && result != NetSendResult.Queued) + var headers = new PeerPacketHeaders { - DebugConsole.NewMessage("Failed to send message to "+conn.Name+": " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow); - } + DeliveryMethod = deliveryMethod, + PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None, + Initialization = null + }; + var body = new PeerPacketMessage + { + Buffer = bufAux + }; + SendMsgInternal(conn, headers, body); } - - public override void Disconnect(NetworkConnection conn,string msg=null) + + public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) { if (netServer == null) { return; } - if (!(conn is LidgrenConnection lidgrenConn)) { return; } + if (conn is not LidgrenConnection lidgrenConn) { return; } + if (connectedClients.Contains(lidgrenConn)) { lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); - OnDisconnect?.Invoke(conn, msg); - Steam.SteamManager.StopAuthSession(conn.SteamID); + callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); + if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } } - lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected"); + + lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); } - protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) + protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) { - LidgrenConnection lidgrenConn = conn as LidgrenConnection; - NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - switch (deliveryMethod) - { - case DeliveryMethod.Unreliable: - lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; - break; - case DeliveryMethod.Reliable: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; - break; - case DeliveryMethod.ReliableOrdered: - lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; - break; - } + IWriteMessage msgToSend = new WriteOnlyMessage(); + msgToSend.WriteNetSerializableStruct(headers); + body?.Write(msgToSend); - NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); - lidgrenMsg.Write(msg.Buffer, 0, msg.LengthBytes); - NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); + NetSendResult result = ForwardToLidgren(msgToSend, conn, headers.DeliveryMethod); if (result != NetSendResult.Sent && result != NetSendResult.Queued) { - DebugConsole.NewMessage("Failed to send message to " + conn.Name + ": " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow); + DebugConsole.NewMessage($"Failed to send message to {conn.Endpoint}: {result}", Microsoft.Xna.Framework.Color.Yellow); } } protected override void CheckOwnership(PendingClient pendingClient) { - LidgrenConnection l = pendingClient.Connection as LidgrenConnection; - if (OwnerConnection == null && - IPAddress.IsLoopback(l.NetConnection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && - ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey) + if (OwnerConnection == null + && pendingClient.Connection is LidgrenConnection l + && IPAddress.IsLoopback(l.NetConnection.RemoteEndPoint.Address) + && ownerKey.IsSome() && pendingClient.OwnerKey == ownerKey) { - ownerKey = null; + ownerKey = Option.None(); OwnerConnection = pendingClient.Connection; + callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } } - protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + protected override void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient) { - if (pendingClient.SteamID == null) + if (pendingClient.AccountInfo.AccountId.IsNone()) { bool requireSteamAuth = GameSettings.CurrentConfig.RequireSteamAuthentication; #if DEBUG requireSteamAuth = false; #endif + bool hasSteamAuth = packet.SteamAuthTicket.TryUnwrap(out var ticket); //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) && - !requireSteamAuth) + if ((!SteamManager.IsInitialized || !hasSteamAuth) && !requireSteamAuth) { - pendingClient.Connection.Name = name; - pendingClient.Name = name; - pendingClient.OwnerKey = ownKey; + pendingClient.Name = packet.Name; + pendingClient.OwnerKey = packet.OwnerKey; pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; } else { - Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) + if (!packet.SteamId.TryUnwrap(out var id) || id is not SteamId steamId) { if (requireSteamAuth) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); return; } - else + } + else + { + Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); + if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - steamId = 0; - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + if (requireSteamAuth) + { + RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); + } + else + { + packet.SteamId = Option.None(); + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + } } } - pendingClient.SteamID = steamId; - pendingClient.Connection.Name = name; - pendingClient.Name = name; - pendingClient.OwnerKey = ownKey; + + pendingClient.Connection.SetAccountInfo(new AccountInfo(packet.SteamId.Select(uid => (AccountId)uid))); + pendingClient.Name = packet.Name; + pendingClient.OwnerKey = packet.OwnerKey; pendingClient.AuthSessionStarted = true; } } else { - if (pendingClient.SteamID != steamId) + if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid)) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); - return; + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); } } } + + private NetSendResult ForwardToLidgren(IWriteMessage msg, NetworkConnection connection, DeliveryMethod deliveryMethod) + { + ToolBox.ThrowIfNull(netServer); + + LidgrenConnection conn = (LidgrenConnection)connection; + return netServer.SendMessage(msg.ToLidgren(netServer), conn.NetConnection, deliveryMethod.ToLidgren()); + } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 93f931061..fb81b889c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -1,75 +1,61 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; using System.Linq; -using System.Text; +using Barotrauma.Extensions; namespace Barotrauma.Networking { - abstract class ServerPeer + internal abstract class ServerPeer { - public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); - public delegate void DisconnectCallback(NetworkConnection connection, string reason); - public delegate void InitializationCompleteCallback(NetworkConnection connection); - public delegate void ShutdownCallback(); - public delegate void OwnerDeterminedCallback(NetworkConnection connection); + public readonly record struct Callbacks( + Callbacks.MessageCallback OnMessageReceived, + Callbacks.DisconnectCallback OnDisconnect, + Callbacks.InitializationCompleteCallback OnInitializationComplete, + Callbacks.ShutdownCallback OnShutdown, + Callbacks.OwnerDeterminedCallback OnOwnerDetermined) + { + public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); + public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket); + public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName); + public delegate void ShutdownCallback(); + public delegate void OwnerDeterminedCallback(NetworkConnection connection); + } - public MessageCallback OnMessageReceived; - public DisconnectCallback OnDisconnect; - public InitializationCompleteCallback OnInitializationComplete; - public ShutdownCallback OnShutdown; - public OwnerDeterminedCallback OnOwnerDetermined; - - protected int? ownerKey; - - public NetworkConnection OwnerConnection { get; protected set; } + protected readonly Callbacks callbacks; + protected ServerPeer(Callbacks callbacks) + { + this.callbacks = callbacks; + } + public abstract void InitializeSteamServerCallbacks(); public abstract void Start(); - public abstract void Close(string msg = null); + public abstract void Close(); public abstract void Update(float deltaTime); - public class PendingClient + protected sealed class PendingClient { - public string Name; - public int OwnerKey; - public NetworkConnection Connection; + public string? Name; + public Option OwnerKey; + public readonly NetworkConnection Connection; public ConnectionInitialization InitializationStep; public double UpdateTime; public double TimeOut; public int Retries; - private UInt64? steamId; - public UInt64? SteamID - { - get { return steamId; } - set - { - steamId = value; - Connection.SetSteamIDIfUnknown(value ?? 0); - } - } - private UInt64? ownerSteamId; - public UInt64? OwnerSteamID - { - get { return ownerSteamId; } - set - { - ownerSteamId = value; - Connection.SetOwnerSteamIDIfUnknown(value ?? 0); - } - } public Int32? PasswordSalt; public bool AuthSessionStarted; + + public AccountInfo AccountInfo => Connection.AccountInfo; public PendingClient(NetworkConnection conn) { - OwnerKey = 0; + OwnerKey = Option.None(); Connection = conn; InitializationStep = ConnectionInitialization.SteamTicketAndVersion; Retries = 0; - SteamID = null; - OwnerSteamID = null; PasswordSalt = null; UpdateTime = Timing.TotalTime + Timing.Step * 3.0; TimeOut = NetworkConnection.TimeoutThreshold; @@ -81,73 +67,70 @@ namespace Barotrauma.Networking TimeOut = NetworkConnection.TimeoutThreshold; } } - protected List connectedClients; - protected List pendingClients; - protected ServerSettings serverSettings; + protected List connectedClients = null!; + protected List pendingClients = null!; + protected ServerSettings serverSettings = null!; + protected Option ownerKey = null!; + protected NetworkConnection? OwnerConnection; - protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc) + protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) { pendingClient.TimeOut = NetworkConnection.TimeoutThreshold; - ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); - - if (pendingClient.InitializationStep != initializationStep) return; + if (pendingClient.InitializationStep != initializationStep) { return; } pendingClient.UpdateTime = Timing.TotalTime + Timing.Step; switch (initializationStep) { case ConnectionInitialization.SteamTicketAndVersion: - string name = Client.SanitizeName(inc.ReadString()); - int ownerKey = inc.ReadInt32(); - UInt64 steamId = inc.ReadUInt64(); - UInt16 ticketLength = inc.ReadUInt16(); - byte[] ticketBytes = inc.ReadBytes(ticketLength); + var authPacket = INetSerializableStruct.Read(inc); - if (!Client.IsValidName(name, serverSettings)) + if (!Client.IsValidName(authPacket.Name, serverSettings)) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName, ""); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName)); return; } - string version = inc.ReadString(); - bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; + bool isCompatibleVersion = + Version.TryParse(authPacket.GameVersion, out var remoteVersion) + && NetworkMember.IsCompatible(remoteVersion, GameMain.Version); if (!isCompatibleVersion) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion()); - GameServer.Log($"{name} ({steamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage($"{name} ({steamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } - LanguageIdentifier language = inc.ReadIdentifier().ToLanguageIdentifier(); - pendingClient.Connection.Language = language; + pendingClient.Connection.Language = authPacket.Language.ToLanguageIdentifier(); - Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower())); if (nameTaken != null) { - RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); - GameServer.Log($"{name} ({steamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.NameTaken)); + GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); return; } if (!pendingClient.AuthSessionStarted) { - ProcessAuthTicket(name, ownerKey, steamId, pendingClient, ticketBytes); + ProcessAuthTicket(authPacket, pendingClient); } + break; case ConnectionInitialization.Password: - int pwLength = inc.ReadByte(); - byte[] incPassword = inc.ReadBytes(pwLength); - if (pendingClient.PasswordSalt == null) + var passwordPacket = INetSerializableStruct.Read(inc); + + if (pendingClient.PasswordSalt is null) { DebugConsole.ThrowError("Received password message from client without salt"); return; } - if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) + + if (serverSettings.IsPasswordCorrect(passwordPacket.Password, pendingClient.PasswordSalt.Value)) { pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; } @@ -156,13 +139,13 @@ namespace Barotrauma.Networking pendingClient.Retries++; if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan) { - string banMsg = "Failed to enter correct password too many times"; + const string banMsg = "Failed to enter correct password too many times"; BanPendingClient(pendingClient, banMsg, null); - - RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg)); return; } } + pendingClient.UpdateTime = Timing.TotalTime; break; case ConnectionInitialization.ContentPackageOrder: @@ -172,37 +155,49 @@ namespace Barotrauma.Networking } } - protected abstract void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket); + protected abstract void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient); protected void BanPendingClient(PendingClient pendingClient, string banReason, TimeSpan? duration) { - if (pendingClient.Connection is LidgrenConnection l) + void banAccountId(AccountId accountId) { - serverSettings.BanList.BanPlayer(pendingClient.Name, l.NetConnection.RemoteEndPoint.Address, banReason, duration); + serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", accountId, banReason, duration); } - else if (pendingClient.Connection is SteamP2PConnection s) + + if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var id)) { banAccountId(id); } + + pendingClient.AccountInfo.OtherMatchingIds.ForEach(banAccountId); + if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var accountId)) { - serverSettings.BanList.BanPlayer(pendingClient.Name, s.SteamID, banReason, duration); - serverSettings.BanList.BanPlayer(pendingClient.Name, s.OwnerSteamID, banReason, duration); + serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", accountId, banReason, duration); + } + else + { + serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", pendingClient.Connection.Endpoint, banReason, duration); } } - protected bool IsPendingClientBanned(PendingClient pendingClient, out string banReason) + protected bool IsPendingClientBanned(PendingClient pendingClient, out string? banReason) { - if (pendingClient.Connection is LidgrenConnection l) + bool isAccountIdBanned(AccountId accountId, out string? banReason) { - return serverSettings.BanList.IsBanned(l.NetConnection.RemoteEndPoint.Address, out banReason); + return serverSettings.BanList.IsBanned(accountId, out banReason); } - else if (pendingClient.Connection is SteamP2PConnection s) + + banReason = default; + bool isBanned = pendingClient.AccountInfo.AccountId.TryUnwrap(out var id) + && isAccountIdBanned(id, out banReason); + foreach (var otherId in pendingClient.AccountInfo.OtherMatchingIds) { - return serverSettings.BanList.IsBanned(s.SteamID, out banReason) || - serverSettings.BanList.IsBanned(s.OwnerSteamID, out banReason); + if (isBanned) { break; } + + isBanned |= isAccountIdBanned(otherId, out banReason); } - banReason = null; - return false; + + return isBanned; } - protected abstract void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg); + protected abstract void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body); protected void UpdatePendingClient(PendingClient pendingClient) { @@ -213,12 +208,12 @@ namespace Barotrauma.Networking if (!skipRemove && connectedClients.Count >= serverSettings.MaxPlayers) { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull)); } - if (IsPendingClientBanned(pendingClient, out string banReason)) + if (IsPendingClientBanned(pendingClient, out string? banReason)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); return; } @@ -228,80 +223,86 @@ namespace Barotrauma.Networking connectedClients.Add(newConnection); pendingClients.Remove(pendingClient); - CheckOwnership(pendingClient); + callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name); - OnInitializationComplete?.Invoke(newConnection); + CheckOwnership(pendingClient); } pendingClient.TimeOut -= Timing.Step; if (pendingClient.TimeOut < 0.0) { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } if (Timing.TotalTime < pendingClient.UpdateTime) { return; } + pendingClient.UpdateTime = Timing.TotalTime + 1.0; - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | - PacketHeader.IsServerMessage)); - outMsg.Write((byte)pendingClient.InitializationStep); + PeerPacketHeaders headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage, + Initialization = pendingClient.InitializationStep + }; + + INetSerializableStruct? structToSend = null; + switch (pendingClient.InitializationStep) { case ConnectionInitialization.ContentPackageOrder: - outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToList(); - outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); - for (int i = 0; i < mpContentPackages.Count; i++) + DateTime timeNow = DateTime.UtcNow; + structToSend = new ServerPeerContentPackageOrderPacket { - outMsg.Write(mpContentPackages[i].Name); - byte[] hashBytes = mpContentPackages[i].Hash.ByteRepresentation; - outMsg.WriteVariableUInt32((UInt32)hashBytes.Length); - outMsg.Write(hashBytes, 0, hashBytes.Length); - outMsg.Write(mpContentPackages[i].SteamWorkshopId); - UInt32 installTimeDiffSeconds = (UInt32)((mpContentPackages[i].InstallTime ?? DateTime.UtcNow) - DateTime.UtcNow).TotalSeconds; - outMsg.Write(installTimeDiffSeconds); - } + ServerName = GameMain.Server.ServerName, + ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) + .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) + .ToImmutableArray() + }; + break; case ConnectionInitialization.Password: - outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); - if (pendingClient.PasswordSalt == null) + structToSend = new ServerPeerPasswordPacket { - pendingClient.PasswordSalt = Lidgren.Network.CryptoRandom.Instance.Next(); - outMsg.Write(pendingClient.PasswordSalt.Value); - } - else + Salt = GetSalt(pendingClient), + RetriesLeft = Option.Some(pendingClient.Retries) + }; + + static Option GetSalt(PendingClient client) { - outMsg.Write(pendingClient.Retries); + if (client.PasswordSalt is { } salt) { return Option.Some(salt); } + + salt = Lidgren.Network.CryptoRandom.Instance.Next(); + client.PasswordSalt = salt; + return Option.Some(salt); } + break; } - SendMsgInternal(pendingClient.Connection, DeliveryMethod.Reliable, outMsg); + SendMsgInternal(pendingClient.Connection, headers, structToSend); } protected virtual void CheckOwnership(PendingClient pendingClient) { } - public void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) + protected void RemovePendingClient(PendingClient pendingClient, PeerDisconnectPacket peerDisconnectPacket) { if (pendingClients.Contains(pendingClient)) { - Disconnect(pendingClient.Connection, reason + "/" + msg); + Disconnect(pendingClient.Connection, peerDisconnectPacket); pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted) + if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some { Value: SteamId steamId }) { - Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value); - pendingClient.SteamID = null; - pendingClient.OwnerSteamID = null; + Steam.SteamManager.StopAuthSession(steamId); + pendingClient.Connection.SetAccountInfo(AccountInfo.None); pendingClient.AuthSessionStarted = false; } } } public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); - public abstract void Disconnect(NetworkConnection conn, string msg = null); + public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket); } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 8718bc4d5..b51065601 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -1,70 +1,61 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Net; -using System.Linq; -using System.Threading; namespace Barotrauma.Networking { - class SteamP2PServerPeer : ServerPeer + internal sealed class SteamP2PServerPeer : ServerPeer { private bool started; - public UInt64 OwnerSteamID - { - get; - private set; - } + private readonly SteamId ownerSteamId; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Value); - - private UInt64 ReadSteamId(IReadMessage inc) - => inc.ReadUInt64() ^ ownerKey64; - private void WriteSteamId(IWriteMessage msg, UInt64 val) - => msg.Write(val ^ ownerKey64); + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - public SteamP2PServerPeer(UInt64 steamId, int ownerKey, ServerSettings settings) + private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); + private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64); + + public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) { serverSettings = settings; connectedClients = new List(); pendingClients = new List(); - this.ownerKey = ownerKey; + this.ownerKey = Option.Some(ownerKey); - OwnerSteamID = steamId; + ownerSteamId = steamId; started = false; } public override void Start() { - IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, OwnerSteamID); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage)); - - byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); - Array.Resize(ref msgToSend, outMsg.LengthBytes); - ChildServerRelay.Write(msgToSend); + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage, + Initialization = null + }; + SendMsgInternal(ownerSteamId, headers, null); started = true; } - public override void Close(string msg = null) + public override void Close() { if (!started) { return; } - if (OwnerConnection != null) OwnerConnection.Status = NetworkConnectionStatus.Disconnected; + if (OwnerConnection != null) { OwnerConnection.Status = NetworkConnectionStatus.Disconnected; } for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); + RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } pendingClients.Clear(); @@ -72,27 +63,21 @@ namespace Barotrauma.Networking ChildServerRelay.ShutDown(); - OnShutdown?.Invoke(); + callbacks.OnShutdown.Invoke(); } public override void Update(float deltaTime) { if (!started) { return; } - if (OnOwnerDetermined != null && OwnerConnection != null) - { - OnOwnerDetermined?.Invoke(OwnerConnection); - OnOwnerDetermined = null; - } - //backwards for loop so we can remove elements while iterating for (int i = connectedClients.Count - 1; i >= 0; i--) { - SteamP2PConnection conn = connectedClients[i] as SteamP2PConnection; + SteamP2PConnection conn = (SteamP2PConnection)connectedClients[i]; conn.Decay(deltaTime); if (conn.Timeout < 0.0) { - Disconnect(conn, "Timed out"); + Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } } @@ -109,7 +94,7 @@ namespace Barotrauma.Networking catch (Exception e) { string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce($"SteamP2PServerPeer.Update:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #else @@ -124,130 +109,132 @@ namespace Barotrauma.Networking if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; } } } - + private void HandleDataMessage(IReadMessage inc) { if (!started) { return; } - UInt64 senderSteamId = ReadSteamId(inc); - UInt64 ownerSteamId = ReadSteamId(inc); + SteamId senderSteamId = ReadSteamId(inc); + SteamId sentOwnerSteamId = ReadSteamId(inc); + + var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); - PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); - if (packetHeader.IsServerMessage()) { - DebugConsole.ThrowError("Got server message from" + senderSteamId.ToString()); + DebugConsole.ThrowError($"Got server message from {senderSteamId}"); return; } - if (senderSteamId != OwnerSteamID) //sender is remote, handle disconnects and heartbeats + if (senderSteamId != ownerSteamId) //sender is remote, handle disconnects and heartbeats { - PendingClient pendingClient = pendingClients.Find(c => c.SteamID == senderSteamId); - SteamP2PConnection connectedClient = connectedClients.Find(c => c.SteamID == senderSteamId) as SteamP2PConnection; - + bool connectionMatches(NetworkConnection conn) => + conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var steamId } } + && steamId == senderSteamId; + + PendingClient? pendingClient = pendingClients.Find(c => connectionMatches(c.Connection)); + SteamP2PConnection? connectedClient = connectedClients.Find(connectionMatches) as SteamP2PConnection; + pendingClient?.Heartbeat(); connectedClient?.Heartbeat(); - string banReason; - if (serverSettings.BanList.IsBanned(senderSteamId, out banReason) || - serverSettings.BanList.IsBanned(ownerSteamId, out banReason)) + if (packetHeader.IsConnectionInitializationStep()) + { + if (!initialization.HasValue) { return; } + ConnectionInitialization initializationStep = initialization.Value; + + if (pendingClient != null) + { + pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); + ReadConnectionInitializationStep( + pendingClient, + new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false), + initializationStep); + } + else if (initializationStep == ConnectionInitialization.ConnectionStarted) + { + pendingClient = new PendingClient(new SteamP2PConnection(senderSteamId)); + pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); + pendingClients.Add(pendingClient); + } + } + else if (serverSettings.BanList.IsBanned(senderSteamId, out string banReason) || + serverSettings.BanList.IsBanned(sentOwnerSteamId, out banReason)) { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); } else if (connectedClient != null) { - Disconnect(connectedClient, DisconnectReason.Banned.ToString() + "/ "+ banReason); + Disconnect(connectedClient, PeerDisconnectPacket.Banned(banReason)); } - return; } else if (packetHeader.IsDisconnectMessage()) { if (pendingClient != null) { - string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; - RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } else if (connectedClient != null) { - string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={connectedClient.Name}"; - Disconnect(connectedClient, disconnectMsg, false); + Disconnect(connectedClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } - return; } else if (packetHeader.IsHeartbeatMessage()) { //message exists solely as a heartbeat, ignore its contents return; } - else if (packetHeader.IsConnectionInitializationStep()) - { - - if (pendingClient != null) - { - if (ownerSteamId != 0) - { - pendingClient.Connection.SetOwnerSteamIDIfUnknown(ownerSteamId); - } - ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Buffer, false, inc.BytePosition, inc.LengthBytes - inc.BytePosition, null)); - } - else - { - ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); - if (initializationStep == ConnectionInitialization.ConnectionStarted) - { - pendingClients.Add(new PendingClient(new SteamP2PConnection("PENDING", senderSteamId)) { SteamID = senderSteamId }); - } - } - } else if (connectedClient != null) { - UInt16 length = inc.ReadUInt16(); - - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, connectedClient); - OnMessageReceived?.Invoke(connectedClient, msg); + var packet = INetSerializableStruct.Read(inc); + IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient); + callbacks.OnMessageReceived.Invoke(connectedClient, msg); } } else //sender is owner { - if (OwnerConnection != null) { (OwnerConnection as SteamP2PConnection).Heartbeat(); } + (OwnerConnection as SteamP2PConnection)?.Heartbeat(); if (packetHeader.IsDisconnectMessage()) { DebugConsole.ThrowError("Received disconnect message from owner"); return; } + if (packetHeader.IsServerMessage()) { DebugConsole.ThrowError("Received server message from owner"); return; } + if (packetHeader.IsConnectionInitializationStep()) { - if (OwnerConnection == null) + if (OwnerConnection is null) { - string ownerName = inc.ReadString(); - OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) + var packet = INetSerializableStruct.Read(inc); + OwnerConnection = new SteamP2PConnection(ownerSteamId) { Language = GameSettings.CurrentConfig.Language }; - OwnerConnection.SetOwnerSteamIDIfUnknown(OwnerSteamID); + OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId)); - OnInitializationComplete?.Invoke(OwnerConnection); + callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.OwnerName); + callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } + return; } + if (packetHeader.IsHeartbeatMessage()) { return; } else { - UInt16 length = inc.ReadUInt16(); - - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, OwnerConnection); - OnMessageReceived?.Invoke(OwnerConnection, msg); + var packet = INetSerializableStruct.Read(inc); + IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, OwnerConnection); + callbacks.OnMessageReceived.Invoke(OwnerConnection!, msg); } } } @@ -256,90 +243,104 @@ namespace Barotrauma.Networking { throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); } - + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!started) { return; } - if (!(conn is SteamP2PConnection steamp2pConn)) return; - if (!connectedClients.Contains(steamp2pConn) && conn != OwnerConnection) + if (conn is not SteamP2PConnection steamP2PConn) { return; } + + if (!connectedClients.Contains(steamP2PConn) && conn != OwnerConnection) { - DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + steamp2pConn.SteamID.ToString()); + DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {steamP2PConn.AccountInfo.AccountId}"); return; } - IWriteMessage msgToSend = new WriteOnlyMessage(); - byte[] msgData = new byte[16]; - msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - WriteSteamId(msgToSend, conn.SteamID); - msgToSend.Write((byte)deliveryMethod); - msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); - msgToSend.Write((UInt16)length); - msgToSend.Write(msgData, 0, length); + if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || connAccountId is not SteamId) { return; } - byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); - Array.Resize(ref bufToSend, msgToSend.LengthBytes); - ChildServerRelay.Write(bufToSend); + byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); + + var headers = new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = (isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) + | PacketHeader.IsServerMessage, + Initialization = null + }; + var body = new PeerPacketMessage + { + Buffer = bufAux + }; + SendMsgInternal(steamP2PConn, headers, body); } - private void SendDisconnectMessage(UInt64 steamId, string msg) - { - if (!started) { return; } - if (string.IsNullOrWhiteSpace(msg)) { return; } - - IWriteMessage msgToSend = new WriteOnlyMessage(); - WriteSteamId(msgToSend, steamId); - msgToSend.Write((byte)DeliveryMethod.Reliable); - msgToSend.Write((byte)(PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage)); - msgToSend.Write(msg); - - byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); - Array.Resize(ref bufToSend, msgToSend.LengthBytes); - ChildServerRelay.Write(bufToSend); - } - - private void Disconnect(NetworkConnection conn, string msg, bool sendDisconnectMessage) + private void SendDisconnectMessage(SteamId steamId, PeerDisconnectPacket peerDisconnectPacket) { if (!started) { return; } - if (!(conn is SteamP2PConnection steamp2pConn)) { return; } - if (sendDisconnectMessage) { SendDisconnectMessage(steamp2pConn.SteamID, msg); } + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage, + Initialization = null + }; + + SendMsgInternal(steamId, headers, peerDisconnectPacket); + } + + public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) + { + if (!started) { return; } + + if (conn is not SteamP2PConnection steamp2pConn) { return; } + + if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || connAccountId is not SteamId connSteamId) { return; } + + SendDisconnectMessage(connSteamId, peerDisconnectPacket); + if (connectedClients.Contains(steamp2pConn)) { steamp2pConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(steamp2pConn); - OnDisconnect?.Invoke(conn, msg); - Steam.SteamManager.StopAuthSession(conn.SteamID); + callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); + Steam.SteamManager.StopAuthSession(connSteamId); } else if (steamp2pConn == OwnerConnection) { - //TODO: fix? + throw new InvalidOperationException("Cannot disconnect owner peer"); } } - public override void Disconnect(NetworkConnection conn, string msg = null) + protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) { - Disconnect(conn, msg, true); - } + var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null; + if (connSteamId is null) { return; } - protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) + SendMsgInternal(connSteamId, headers, body); + } + + private void SendMsgInternal(SteamId connSteamId, PeerPacketHeaders headers, INetSerializableStruct? body) { IWriteMessage msgToSend = new WriteOnlyMessage(); - WriteSteamId(msgToSend, conn.SteamID); - msgToSend.Write((byte)deliveryMethod); - msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); - byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); - Array.Resize(ref bufToSend, msgToSend.LengthBytes); + WriteSteamId(msgToSend, connSteamId); + msgToSend.WriteNetSerializableStruct(headers); + body?.Write(msgToSend); + + ForwardToOwnerProcess(msgToSend); + } + + private static void ForwardToOwnerProcess(IWriteMessage msg) + { + byte[] bufToSend = (byte[])msg.Buffer.Clone(); + Array.Resize(ref bufToSend, msg.LengthBytes); ChildServerRelay.Write(bufToSend); } - protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + protected override void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient) { pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - - pendingClient.Connection.Name = name; - pendingClient.Name = name; + pendingClient.Name = packet.Name; pendingClient.AuthSessionStarted = true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 9007d3462..817c19b59 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -8,11 +8,6 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { - /// - /// How much skills drop towards the job's default skill levels when respawning midround in the campaign - /// - const float SkillReductionOnCampaignMidroundRespawn = 0.75f; - private DateTime despawnTime; private float shuttleEmptyTimer; @@ -132,7 +127,7 @@ namespace Barotrauma.Networking return characterToRespawnCount >= GetMinCharactersToRespawn(); } - partial void UpdateWaiting(float deltaTime) + partial void UpdateWaiting(float _) { if (RespawnShuttle != null) { @@ -465,26 +460,28 @@ namespace Barotrauma.Networking } clients[i].Character = character; - character.OwnerClientEndPoint = clients[i].Connection.EndPointString; + character.OwnerClientAddress = clients[i].Connection.Endpoint.Address; character.OwnerClientName = clients[i].Name; - GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", GameServer.ClientLogName(clients[i]), clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); + GameServer.Log( + $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); } if (RespawnShuttle != null) { - Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position; + List newRespawnItems = new List(); + Vector2 pos = cargoSp?.Position ?? character.Position; if (divingSuitPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); - respawnItems.Add(divingSuit); + newRespawnItems.Add(divingSuit); if (oxyPrefab != null && divingSuit.GetComponent() != null) { var oxyTank = new Item(oxyPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); divingSuit.Combine(oxyTank, user: null); - respawnItems.Add(oxyTank); + newRespawnItems.Add(oxyTank); } } @@ -494,13 +491,13 @@ namespace Barotrauma.Networking { var scooter = new Item(scooterPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); - respawnItems.Add(scooter); + newRespawnItems.Add(scooter); if (batteryPrefab != null) { var battery = new Item(batteryPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); scooter.Combine(battery, user: null); - respawnItems.Add(battery); + newRespawnItems.Add(battery); } } } @@ -508,14 +505,31 @@ namespace Barotrauma.Networking { AutoItemPlacer.RegenerateLoot(RespawnShuttle, respawnContainer); } + + //try to put the items in containers in the shuttle + foreach (var respawnItem in newRespawnItems) + { + System.Diagnostics.Debug.Assert(!respawnItem.Removed); + foreach (Item shuttleItem in RespawnShuttle.GetItems(alsoFromConnectedSubs: false)) + { + if (shuttleItem.NonInteractable || shuttleItem.NonPlayerTeamInteractable) { continue; } + var container = shuttleItem.GetComponent(); + if (container != null && container.Inventory.TryPutItem(respawnItem, user: null)) + { + break; + } + } + respawnItems.Add(respawnItem); + } } var characterData = campaign?.GetClientCharacterData(clients[i]); if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) { + //we need to reapply the previous respawn penalty affliction or successive deaths won't make it stack + characterData.ApplyHealthData(character, (AfflictionPrefab ap) => ap == GetRespawnPenaltyAfflictionPrefab()); GiveRespawnPenaltyAffliction(character); } - if (characterData == null || characterData.HasSpawned) { //give the character the items they would've gotten if they had spawned in the main sub @@ -563,8 +577,8 @@ namespace Barotrauma.Networking foreach (Skill skill in characterInfo.Job.GetSkills()) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); - if (skillPrefab == null) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.Start, SkillReductionOnCampaignMidroundRespawn); + if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath); } } @@ -575,20 +589,20 @@ namespace Barotrauma.Networking switch (CurrentState) { case State.Transporting: - msg.Write(ReturnCountdownStarted); - msg.Write(GameMain.Server.ServerSettings.MaxTransportTime); - msg.Write((float)(ReturnTime - DateTime.Now).TotalSeconds); + msg.WriteBoolean(ReturnCountdownStarted); + msg.WriteSingle(GameMain.Server.ServerSettings.MaxTransportTime); + msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds); break; case State.Waiting: MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; var matchingData = campaign?.GetClientCharacterData(c); bool forceSpawnInMainSub = matchingData != null && !matchingData.HasSpawned; - msg.Write((ushort)pendingRespawnCount); - msg.Write((ushort)requiredRespawnCount); - msg.Write(IsRespawnPromptPendingForClient(c)); - msg.Write(RespawnCountdownStarted); - msg.Write(forceSpawnInMainSub); - msg.Write((float)(RespawnTime - DateTime.Now).TotalSeconds); + msg.WriteUInt16((ushort)pendingRespawnCount); + msg.WriteUInt16((ushort)requiredRespawnCount); + msg.WriteBoolean(IsRespawnPromptPendingForClient(c)); + msg.WriteBoolean(RespawnCountdownStarted); + msg.WriteBoolean(forceSpawnInMainSub); + msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds); break; case State.Returning: break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 3fca00526..a95387e2e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -18,29 +18,39 @@ namespace Barotrauma.Networking { if (!PropEquals(lastSyncedValue, Value)) { - LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID); + LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID; lastSyncedValue = Value; } } + public void ForceUpdate() + { + LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID++; + } } public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly char SubmarineSeparatorChar = '|'; - public readonly Dictionary LastUpdateIdForFlag = new Dictionary(); - public UInt16 LastPropertyUpdateId { get; private set; } = 1; - + public readonly Dictionary LastUpdateIdForFlag + = ((NetFlags[])Enum.GetValues(typeof(NetFlags))) + .Select(f => (f, (ushort)1)) + .ToDictionary(); + public void UpdateFlag(NetFlags flag) => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); + public NetFlags UnsentFlags() + => LastUpdateIdForFlag.Keys + .Where(k => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[k], GameMain.NetLobbyScreen.LastUpdateID)) + .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); + private bool IsFlagRequired(Client c, NetFlags flag) => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) - .Concat(NetFlags.None.ToEnumerable()) //prevents InvalidOperationException in Aggregate - .Aggregate((f1, f2) => f1 | f2); + .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); partial void InitProjSpecific() { @@ -48,6 +58,15 @@ namespace Barotrauma.Networking LoadClientPermissions(); } + public void ForcePropertyUpdate() + { + UpdateFlag(NetFlags.Properties); + foreach (NetPropertyData property in netProperties.Values) + { + property.ForceUpdate(); + } + } + private void WriteNetProperties(IWriteMessage outMsg, Client c) { foreach (UInt32 key in netProperties.Keys) @@ -56,40 +75,39 @@ namespace Barotrauma.Networking property.SyncValue(); if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) { - outMsg.Write(key); + outMsg.WriteUInt32(key); netProperties[key].Write(outMsg); } } - outMsg.Write((UInt32)0); + outMsg.WriteUInt32((UInt32)0); } public void ServerAdminWrite(IWriteMessage outMsg, Client c) { - c.LastSentServerSettingsUpdate = LastPropertyUpdateId; + c.LastSentServerSettingsUpdate = LastUpdateIdForFlag[NetFlags.Properties]; WriteNetProperties(outMsg, c); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); - Whitelist.ServerAdminWrite(outMsg, c); } public void ServerWrite(IWriteMessage outMsg, Client c) { NetFlags requiredFlags = GetRequiredFlags(c); - outMsg.Write((byte)requiredFlags); + outMsg.WriteByte((byte)requiredFlags); if (requiredFlags.HasFlag(NetFlags.Name)) { - outMsg.Write(ServerName); + outMsg.WriteString(ServerName); } if (requiredFlags.HasFlag(NetFlags.Message)) { - outMsg.Write(ServerMessageText); + outMsg.WriteString(ServerMessageText); } - outMsg.Write((byte)PlayStyle); - outMsg.Write((byte)MaxPlayers); - outMsg.Write(HasPassword); - outMsg.Write(IsPublic); - outMsg.Write(AllowFileTransfers); + outMsg.WriteByte((byte)PlayStyle); + outMsg.WriteByte((byte)MaxPlayers); + outMsg.WriteBoolean(HasPassword); + outMsg.WriteBoolean(IsPublic); + outMsg.WriteBoolean(AllowFileTransfers); outMsg.WritePadBits(); outMsg.WriteRangedInteger(TickRate, 1, 60); @@ -104,16 +122,18 @@ namespace Barotrauma.Networking } if (c.HasPermission(Networking.ClientPermissions.ManageSettings) - && !NetIdUtils.IdMoreRecentOrMatches(c.LastRecvServerSettingsUpdate, LastPropertyUpdateId)) + && NetIdUtils.IdMoreRecent( + newID: LastUpdateIdForFlag[NetFlags.Properties], + oldID: c.LastRecvServerSettingsUpdate)) { - outMsg.Write(true); + outMsg.WriteBoolean(true); outMsg.WritePadBits(); ServerAdminWrite(outMsg, c); } else { - outMsg.Write(false); + outMsg.WriteBoolean(false); outMsg.WritePadBits(); } } @@ -171,12 +191,10 @@ namespace Barotrauma.Networking propertiesChanged |= changedMonsterSettings; if (changedMonsterSettings) { ReadMonsterEnabled(incMsg); } propertiesChanged |= BanList.ServerAdminRead(incMsg, c); - propertiesChanged |= Whitelist.ServerAdminRead(incMsg, c); if (propertiesChanged) { UpdateFlag(NetFlags.Properties); - LastPropertyUpdateId = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); } changed |= propertiesChanged; } @@ -192,27 +210,32 @@ namespace Barotrauma.Networking { int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; - GameMain.NetLobbyScreen.MissionType = (Barotrauma.MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); + GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; - if (traitorSetting < 0) traitorSetting = 2; - if (traitorSetting > 2) traitorSetting = 0; + if (traitorSetting < 0) { traitorSetting = 2; } + if (traitorSetting > 2) { traitorSetting = 0; } TraitorsEnabled = (YesNoMaybe)traitorSetting; int botCount = BotCount + incMsg.ReadByte() - 1; - if (botCount < 0) botCount = MaxBotCount; - if (botCount > MaxBotCount) botCount = 0; + if (botCount < 0) { botCount = MaxBotCount; } + if (botCount > MaxBotCount) { botCount = 0; } BotCount = botCount; int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; - if (botSpawnMode < 0) botSpawnMode = 1; - if (botSpawnMode > 1) botSpawnMode = 0; + if (botSpawnMode < 0) { botSpawnMode = 1; } + if (botSpawnMode > 1) { botSpawnMode = 0; } BotSpawnMode = (BotSpawnMode)botSpawnMode; float levelDifficulty = incMsg.ReadSingle(); - if (levelDifficulty >= 0.0f) SelectedLevelDifficulty = levelDifficulty; + if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; } - UseRespawnShuttle = incMsg.ReadBoolean(); + bool changedUseRespawnShuttle = incMsg.ReadBoolean(); + bool useRespawnShuttle = incMsg.ReadBoolean(); + if (changedUseRespawnShuttle) + { + UseRespawnShuttle = useRespawnShuttle; + } bool changedAutoRestart = incMsg.ReadBoolean(); bool autoRestart = incMsg.ReadBoolean(); @@ -444,31 +467,27 @@ namespace Barotrauma.Networking { ClientPermissions.Clear(); - if (!File.Exists(ClientPermissionsFile)) - { - if (File.Exists("Data/clientpermissions.txt")) - { - LoadClientPermissionsOld("Data/clientpermissions.txt"); - } - return; - } + if (!File.Exists(ClientPermissionsFile)) { return; } XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile); if (doc == null) { return; } foreach (XElement clientElement in doc.Root.Elements()) { string clientName = clientElement.GetAttributeString("name", ""); - string clientEndPoint = clientElement.GetAttributeString("endpoint", null) ?? clientElement.GetAttributeString("ip", ""); - string steamIdStr = clientElement.GetAttributeString("steamid", ""); + string addressStr = clientElement.GetAttributeString("address", null) + ?? clientElement.GetAttributeString("endpoint", null) + ?? clientElement.GetAttributeString("ip", ""); + string accountIdStr = clientElement.GetAttributeString("accountid", null) + ?? clientElement.GetAttributeString("steamid", ""); if (string.IsNullOrWhiteSpace(clientName)) { - DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name and an IP address."); + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name."); continue; } - if (string.IsNullOrWhiteSpace(clientEndPoint) && string.IsNullOrWhiteSpace(steamIdStr)) + if (string.IsNullOrWhiteSpace(addressStr) && string.IsNullOrWhiteSpace(accountIdStr)) { - DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an IP address or a Steam ID."); + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an endpoint or a Steam ID."); continue; } @@ -542,69 +561,33 @@ namespace Barotrauma.Networking } } - if (!string.IsNullOrEmpty(steamIdStr)) + if (!string.IsNullOrEmpty(accountIdStr)) { - if (ulong.TryParse(steamIdStr, out ulong steamID)) + if (AccountId.Parse(accountIdStr).TryUnwrap(out var accountId)) { - ClientPermissions.Add(new SavedClientPermission(clientName, steamID, permissions, permittedCommands)); + ClientPermissions.Add(new SavedClientPermission(clientName, accountId, permissions, permittedCommands)); } else { - DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + steamIdStr + "\" is not a valid Steam ID."); - continue; + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + accountIdStr + "\" is not a valid account ID."); } } else { - ClientPermissions.Add(new SavedClientPermission(clientName, clientEndPoint, permissions, permittedCommands)); - } - } - } - - /// - /// Method for loading old .txt client permission files to provide backwards compatibility - /// - private void LoadClientPermissionsOld(string file) - { - if (!File.Exists(file)) return; - - string[] lines; - try - { - lines = File.ReadAllLines(file); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to open client permission file " + ClientPermissionsFile, e); - return; - } - - ClientPermissions.Clear(); - - foreach (string line in lines) - { - string[] separatedLine = line.Split('|'); - if (separatedLine.Length < 3) { continue; } - - string name = string.Join("|", separatedLine.Take(separatedLine.Length - 2)); - string ip = separatedLine[separatedLine.Length - 2]; - - ClientPermissions permissions; - if (Enum.TryParse(separatedLine.Last(), out permissions)) - { - ClientPermissions.Add(new SavedClientPermission(name, ip, permissions, new HashSet())); + if (Address.Parse(addressStr).TryUnwrap(out var address)) + { + ClientPermissions.Add(new SavedClientPermission(clientName, address, permissions, permittedCommands)); + } + else + { + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + addressStr + "\" is not a valid endpoint."); + } } } } public void SaveClientPermissions() { - //delete old client permission file - if (File.Exists("Data/clientpermissions.txt")) - { - File.Delete("Data/clientpermissions.txt"); - } - GameServer.Log("Saving client permissions", ServerLog.MessageType.ServerMessage); XDocument doc = new XDocument(new XElement("ClientPermissions")); @@ -612,6 +595,7 @@ namespace Barotrauma.Networking foreach (SavedClientPermission clientPermission in ClientPermissions) { var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); + #warning TODO: this is broken because of localization if (matchingPreset != null && matchingPreset.Name == "None") { continue; @@ -620,23 +604,14 @@ namespace Barotrauma.Networking XElement clientElement = new XElement("Client", new XAttribute("name", clientPermission.Name)); - if (clientPermission.SteamID > 0) - { - clientElement.Add(new XAttribute("steamid", clientPermission.SteamID)); - } - else - { - clientElement.Add(new XAttribute("endpoint", clientPermission.EndPoint)); - } + clientElement.Add(clientPermission.AddressOrAccountId.TryGet(out AccountId accountId) + ? new XAttribute("accountid", accountId.StringRepresentation) + : new XAttribute("address", ((Address)clientPermission.AddressOrAccountId).StringRepresentation)); - if (matchingPreset == null) - { - clientElement.Add(new XAttribute("permissions", clientPermission.Permissions.ToString())); - } - else - { - clientElement.Add(new XAttribute("preset", matchingPreset.Name)); - } + clientElement.Add(matchingPreset == null + ? new XAttribute("permissions", clientPermission.Permissions.ToString()) + : new XAttribute("preset", matchingPreset.Name)); + if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { foreach (DebugConsole.Command command in clientPermission.PermittedCommands) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index 3e3474ab3..33fa6dfad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -56,8 +56,8 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ServerPacketHeader.VOICE); - msg.Write((byte)queue.QueueID); + msg.WriteByte((byte)ServerPacketHeader.VOICE); + msg.WriteByte((byte)queue.QueueID); queue.Write(msg); netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 4724420b4..be597c2da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -216,6 +216,14 @@ namespace Barotrauma } } + public void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) + { + foreach (Client client in connectedClients) + { + client.ResetVotes(resetKickVotes); + } + } + public void ServerRead(IReadMessage inc, Client sender) { if (GameMain.Server == null || sender == null) { return; } @@ -254,15 +262,20 @@ namespace Barotrauma break; case VoteType.Kick: byte kickedClientID = inc.ReadByte(); - - Client kicked = GameMain.Server.ConnectedClients.Find(c => c.ID == kickedClientID); - if (kicked != null && kicked.Connection != GameMain.Server.OwnerConnection && !kicked.HasKickVoteFrom(sender)) + if ((DateTime.Now - sender.JoinTime).TotalSeconds < GameMain.Server.ServerSettings.DisallowKickVoteTime) { - kicked.AddKickVote(sender); - Client.UpdateKickVotes(GameMain.Server.ConnectedClients); - GameMain.Server.SendChatMessage($"ServerMessage.HasVotedToKick~[initiator]={sender.Name}~[target]={kicked.Name}", ChatMessageType.Server, null); + GameMain.Server.SendDirectChatMessage($"ServerMessage.kickvotedisallowed", sender); + } + else + { + Client kicked = GameMain.Server.ConnectedClients.Find(c => c.SessionId == kickedClientID); + if (kicked != null && kicked.Connection != GameMain.Server.OwnerConnection && !kicked.HasKickVoteFrom(sender)) + { + kicked.AddKickVote(sender); + Client.UpdateKickVotes(GameMain.Server.ConnectedClients); + GameMain.Server.SendChatMessage($"ServerMessage.HasVotedToKick~[initiator]={sender.Name}~[target]={kicked.Name}", ChatMessageType.Server, null); + } } - break; case VoteType.StartRound: bool ready = inc.ReadBoolean(); @@ -287,9 +300,9 @@ namespace Barotrauma if (!ShouldRejectVote(sender, voteType)) { pendingVotes.Enqueue(new TransferVote(sender, - GameMain.Server.ConnectedClients.Find(c => c.ID == fromClientId), + GameMain.Server.ConnectedClients.Find(c => c.SessionId == fromClientId), amount, - GameMain.Server.ConnectedClients.Find(c => c.ID == toClientId))); + GameMain.Server.ConnectedClients.Find(c => c.SessionId == toClientId))); } } else @@ -323,66 +336,66 @@ namespace Barotrauma { if (GameMain.Server == null) { return; } - msg.Write(GameMain.Server.ServerSettings.AllowSubVoting); + msg.WriteBoolean(GameMain.Server.ServerSettings.AllowSubVoting); if (GameMain.Server.ServerSettings.AllowSubVoting) { IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Sub, GameMain.Server.ConnectedClients); - msg.Write((byte)voteList.Count); + msg.WriteByte((byte)voteList.Count); foreach (KeyValuePair vote in voteList) { - msg.Write((byte)vote.Value); - msg.Write(vote.Key.Name); + msg.WriteByte((byte)vote.Value); + msg.WriteString(vote.Key.Name); } } - msg.Write(GameMain.Server.ServerSettings.AllowModeVoting); + msg.WriteBoolean(GameMain.Server.ServerSettings.AllowModeVoting); if (GameMain.Server.ServerSettings.AllowModeVoting) { IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Mode, GameMain.Server.ConnectedClients); - msg.Write((byte)voteList.Count); + msg.WriteByte((byte)voteList.Count); foreach (KeyValuePair vote in voteList) { - msg.Write((byte)vote.Value); - msg.Write(vote.Key.Identifier); + msg.WriteByte((byte)vote.Value); + msg.WriteIdentifier(vote.Key.Identifier); } } - msg.Write(GameMain.Server.ServerSettings.AllowEndVoting); + msg.WriteBoolean(GameMain.Server.ServerSettings.AllowEndVoting); if (GameMain.Server.ServerSettings.AllowEndVoting) { - msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound))); - msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned)); + msg.WriteByte((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound))); + msg.WriteByte((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned)); } - msg.Write(GameMain.Server.ServerSettings.AllowVoteKick); + msg.WriteBoolean(GameMain.Server.ServerSettings.AllowVoteKick); - msg.Write((byte)(ActiveVote?.State ?? VoteState.None)); + msg.WriteByte((byte)(ActiveVote?.State ?? VoteState.None)); if (ActiveVote != null) { - msg.Write((byte)ActiveVote.VoteType); + msg.WriteByte((byte)ActiveVote.VoteType); if (ActiveVote.State != VoteState.None && ActiveVote.VoteType != VoteType.Unknown) { var eligibleClients = GameMain.Server.ConnectedClients.Where(c => c.InGame && c != ActiveVote.VoteStarter); var yesClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 2); - msg.Write((byte)yesClients.Count()); + msg.WriteByte((byte)yesClients.Count()); foreach (Client c in yesClients) { - msg.Write(c.ID); + msg.WriteByte(c.SessionId); } var noClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 1); - msg.Write((byte)noClients.Count()); + msg.WriteByte((byte)noClients.Count()); foreach (Client c in noClients) { - msg.Write(c.ID); + msg.WriteByte(c.SessionId); } - msg.Write((byte)eligibleClients.Count()); + msg.WriteByte((byte)eligibleClients.Count()); switch (ActiveVote.State) { case VoteState.Started: - msg.Write(ActiveVote.VoteStarter.ID); - msg.Write((byte)GameMain.Server.ServerSettings.VoteTimeout); + msg.WriteByte(ActiveVote.VoteStarter.SessionId); + msg.WriteByte((byte)GameMain.Server.ServerSettings.VoteTimeout); switch (ActiveVote.VoteType) { @@ -390,14 +403,14 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: SubmarineVote vote = ActiveVote as SubmarineVote; - msg.Write(vote.Sub.Name); - msg.Write(vote.TransferItems); + msg.WriteString(vote.Sub.Name); + msg.WriteBoolean(vote.TransferItems); break; case VoteType.TransferMoney: var transferVote = (ActiveVote as TransferVote); - msg.Write(transferVote.From?.ID ?? 0); - msg.Write(transferVote.To?.ID ?? 0); - msg.Write(transferVote.TransferAmount); + msg.WriteByte(transferVote.From?.SessionId ?? 0); + msg.WriteByte(transferVote.To?.SessionId ?? 0); + msg.WriteInt32(transferVote.TransferAmount); break; } @@ -407,16 +420,16 @@ namespace Barotrauma break; case VoteState.Passed: case VoteState.Failed: - msg.Write(ActiveVote.State == VoteState.Passed); + msg.WriteBoolean(ActiveVote.State == VoteState.Passed); switch (ActiveVote.VoteType) { case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: var subVote = ActiveVote as SubmarineVote; - msg.Write(subVote.Sub.Name); - msg.Write(subVote.TransferItems); - msg.Write((short)subVote.DeliveryFee); + msg.WriteString(subVote.Sub.Name); + msg.WriteBoolean(subVote.TransferItems); + msg.WriteInt16((short)subVote.DeliveryFee); break; } break; @@ -424,11 +437,11 @@ namespace Barotrauma } } - var readyClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote(VoteType.StartRound)); - msg.Write((byte)readyClients.Count); + var readyClients = GameMain.Server.ConnectedClients.Where(c => c.GetVote(VoteType.StartRound)); + msg.WriteByte((byte)readyClients.Count()); foreach (Client c in readyClients) { - msg.Write(c.ID); + msg.WriteByte(c.SessionId); } msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs deleted file mode 100644 index 0e6d43c44..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Net; - -namespace Barotrauma.Networking -{ - partial class WhiteListedPlayer - { - private static UInt16 LastIdentifier = 0; - - public WhiteListedPlayer(string name,string ip) - { - Name = name; - IP = ip; - - UniqueIdentifier = LastIdentifier; LastIdentifier++; - } - } - - partial class WhiteList - { - partial void InitProjSpecific() - { - if (!File.Exists(SavePath)) { return; } - - string[] lines; - try - { - lines = File.ReadAllLines(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to open whitelist in " + SavePath, e); - return; - } - - foreach (string line in lines) - { - if (line[0] == '#') - { - string lineval = line.Substring(1, line.Length - 1); - Int32.TryParse(lineval, out int intVal); - if (lineval.ToLower() == "true" || intVal != 0) - { - Enabled = true; - } - else - { - Enabled = false; - } - } - else - { - string[] separatedLine = line.Split(','); - if (separatedLine.Length < 2) continue; - - string name = string.Join(",", separatedLine.Take(separatedLine.Length - 1)); - string ip = separatedLine.Last(); - - whitelistedPlayers.Add(new WhiteListedPlayer(name, ip)); - } - } - } - - public void Save() - { - GameServer.Log("Saving whitelist", ServerLog.MessageType.ServerMessage); - - GameMain.Server?.ServerSettings?.UpdateFlag(ServerSettings.NetFlags.Properties); - - List lines = new List(); - - if (Enabled) - { - lines.Add("#true"); - } - else - { - lines.Add("#false"); - } - foreach (WhiteListedPlayer wlp in whitelistedPlayers) - { - lines.Add(wlp.Name + "," + wlp.IP); - } - - try - { - File.WriteAllLines(SavePath, lines); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving the whitelist to " + SavePath + " failed", e); - } - } - - public bool IsWhiteListed(string name, IPAddress address) - { - if (!Enabled) return true; - WhiteListedPlayer wlp = whitelistedPlayers.Find(p => p.Name == name); - if (wlp == null) return false; - if (!string.IsNullOrWhiteSpace(wlp.IP)) - { - if (address.IsIPv4MappedToIPv6 && wlp.IP == address.MapToIPv4NoThrow().ToString()) - { - return true; - } - else - { - return wlp.IP == address.ToString(); - } - } - return true; - } - - private void RemoveFromWhiteList(WhiteListedPlayer wlp) - { - GameServer.Log("Removing " + wlp.Name + " from whitelist", ServerLog.MessageType.ServerMessage); - whitelistedPlayers.Remove(wlp); - } - - private void AddToWhiteList(string name, string ip) - { - if (string.IsNullOrWhiteSpace(name)) return; - if (whitelistedPlayers.Any(x => x.Name.ToLower() == name.ToLower() && x.IP == ip)) return; - whitelistedPlayers.Add(new WhiteListedPlayer(name, ip)); - } - - public void ServerAdminWrite(IWriteMessage outMsg, Client c) - { - if (!c.HasPermission(ClientPermissions.ManageSettings)) - { - outMsg.Write(false); outMsg.WritePadBits(); - return; - } - outMsg.Write(true); - outMsg.Write(c.Connection == GameMain.Server.OwnerConnection); - outMsg.Write(Enabled); - - outMsg.WritePadBits(); - outMsg.WriteVariableUInt32((UInt32)whitelistedPlayers.Count); - for (int i = 0; i < whitelistedPlayers.Count; i++) - { - WhiteListedPlayer whitelistedPlayer = whitelistedPlayers[i]; - - outMsg.Write(whitelistedPlayer.Name); - outMsg.Write(whitelistedPlayer.UniqueIdentifier); - if (c.Connection == GameMain.Server.OwnerConnection) - { - outMsg.Write(whitelistedPlayer.IP); - //outMsg.Write(whitelistedPlayer.SteamID); //TODO: add steamid to whitelisted players - } - } - } - - public bool ServerAdminRead(IReadMessage incMsg, Client c) - { - if (!c.HasPermission(ClientPermissions.ManageSettings)) - { - bool enabled = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - UInt16 removeCount = incMsg.ReadUInt16(); - incMsg.BitPosition += removeCount * 4 * 8; - UInt16 addCount = incMsg.ReadUInt16(); - for (int i = 0; i < addCount; i++) - { - incMsg.ReadString(); //skip name - incMsg.ReadString(); //skip ip - } - return false; - } - else - { - bool prevEnabled = Enabled; - bool enabled = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - Enabled = enabled; - - UInt16 removeCount = incMsg.ReadUInt16(); - for (int i = 0; i < removeCount; i++) - { - UInt16 id = incMsg.ReadUInt16(); - WhiteListedPlayer whitelistedPlayer = whitelistedPlayers.Find(p => p.UniqueIdentifier == id); - if (whitelistedPlayer != null) - { - GameServer.Log(GameServer.ClientLogName(c) + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); - RemoveFromWhiteList(whitelistedPlayer); - } - } - - UInt16 addCount = incMsg.ReadUInt16(); - for (int i = 0; i < addCount; i++) - { - string name = incMsg.ReadString(); - string ip = incMsg.ReadString(); - - GameServer.Log(GameServer.ClientLogName(c) + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage); - AddToWhiteList(name, ip); - } - - bool changed = removeCount > 0 || addCount > 0 || prevEnabled != enabled; - if (changed) { Save(); } - return changed; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs index 0b03fe680..04abe986f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs @@ -11,8 +11,8 @@ namespace Barotrauma float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - msg.Write(SimPosition.X); - msg.Write(SimPosition.Y); + msg.WriteSingle(SimPosition.X); + msg.WriteSingle(SimPosition.Y); #if DEBUG if (Math.Abs(FarseerBody.LinearVelocity.X) > MaxVel || @@ -22,8 +22,8 @@ namespace Barotrauma } #endif - msg.Write(FarseerBody.Awake); - msg.Write(FarseerBody.FixedRotation); + msg.WriteBoolean(FarseerBody.Awake); + msg.WriteBoolean(FarseerBody.FixedRotation); if (!FarseerBody.FixedRotation) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index c5bd63303..b0af47a87 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -55,6 +55,10 @@ namespace Barotrauma #if LINUX setLinuxEnv(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + GameMain.ShouldRun = false; + }; #endif Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); @@ -102,7 +106,7 @@ namespace Barotrauma private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { - void swallowExceptions(Action action) + static void swallowExceptions(Action action) { try { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 70702b830..fbcbd1a50 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -197,7 +197,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients); + GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 26ee21b1d..d51559575 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -1,4 +1,5 @@ using System.Linq; +using Barotrauma.Networking; namespace Barotrauma.Steam { @@ -53,13 +54,13 @@ namespace Barotrauma.Steam Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); - Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText); + Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); - Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString()); + Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); - Steamworks.SteamServer.SetKey("usingwhitelist", (server.ServerSettings.Whitelist != null && server.ServerSettings.Whitelist.Enabled).ToString()); + Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp + => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -77,12 +78,12 @@ namespace Barotrauma.Steam return true; } - public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) + public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, SteamId clientSteamID) { if (!IsInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID); + Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID.Value); if (startResult != Steamworks.BeginAuthResult.OK) { DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); @@ -91,12 +92,12 @@ namespace Barotrauma.Steam return startResult; } - public static void StopAuthSession(ulong clientSteamID) + public static void StopAuthSession(SteamId clientSteamId) { if (!IsInitialized || !Steamworks.SteamServer.IsValid) return; - DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamID); - Steamworks.SteamServer.EndSession(clientSteamID); + DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamId); + Steamworks.SteamServer.EndSession(clientSteamId.Value); } public static bool CloseServer() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs index 90053ec80..c6514d41c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs @@ -28,13 +28,13 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg) { - msg.Write(MissionIdentifier); - msg.Write(EndMessage); - msg.Write(Success); - msg.Write((byte)Characters.Count); + msg.WriteIdentifier(MissionIdentifier); + msg.WriteString(EndMessage); + msg.WriteBoolean(Success); + msg.WriteByte((byte)Characters.Count); foreach (Character character in Characters) { - msg.Write(character.ID); + msg.WriteUInt16(character.ID); } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 85c7e124e..12f6075f1 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -1,25 +1,19 @@ - - - - - Exe - netcoreapp3.1 + net6.0 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.15.0 + 0.19.8.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - Doc\BuildDocServer.xml - en @@ -67,7 +61,7 @@ - + @@ -157,4 +151,4 @@ - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index efff061b6..f8e9bd8f5 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -64,7 +64,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/DeployGameAnalytics.props b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props index 7c7f3bac4..e4818697e 100644 --- a/Barotrauma/BarotraumaShared/DeployGameAnalytics.props +++ b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/ModLists/Release checklist mods.xml b/Barotrauma/BarotraumaShared/ModLists/Release checklist mods.xml new file mode 100644 index 000000000..8a2bef012 --- /dev/null +++ b/Barotrauma/BarotraumaShared/ModLists/Release checklist mods.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 8897ebecb..1708c86da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -87,11 +87,11 @@ namespace Barotrauma if (_attackLimb != value) { _previousAttackLimb = _attackLimb; - _previousAttackLimb?.AttachedRope?.Snap(); + if (_previousAttackLimb != null && _previousAttackLimb.attack.SnapRopeOnNewAttack) { _previousAttackLimb.AttachedRope?.Snap(); } } else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) { - _attackLimb.AttachedRope?.Snap(); + if (_attackLimb != null && _attackLimb.attack.SnapRopeOnNewAttack) { _attackLimb.AttachedRope?.Snap(); } } _attackLimb = value; attackVector = null; @@ -3660,7 +3660,7 @@ namespace Barotrauma targetDir = Vector2.UnitY; } } - float margin = 30000; + float margin = Level.OutsideBoundsCurrentMargin; if (pos.X < -margin) { // Too far left @@ -3847,7 +3847,7 @@ namespace Barotrauma public override void ServerWrite(IWriteMessage msg) { - msg.Write((byte)State); + msg.WriteByte((byte)State); PetBehavior?.ServerWrite(msg); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index adf78a243..13e861573 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -466,7 +466,12 @@ namespace Barotrauma } } } - steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); + + //if someone is grabbing the bot and the bot isn't trying to run anywhere, let them keep dragging and "control" the bot + if (Character.SelectedBy == null || run) + { + steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); + } bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) @@ -511,9 +516,9 @@ namespace Barotrauma { newDir = Direction.Left; } - if (Character.SelectedConstruction != null) + if (Character.SelectedItem != null) { - Character.SelectedConstruction.SecondaryUse(deltaTime, Character); + Character.SelectedItem.SecondaryUse(deltaTime, Character); } } else if (AutoFaceMovement && Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) @@ -2148,7 +2153,7 @@ namespace Barotrauma if (c.Removed) { continue; } if (c.TeamID != team) { continue; } if (c.IsIncapacitated) { continue; } - if (c.SelectedConstruction == target.Item) + if (c.SelectedItem == target.Item) { operatingCharacter = c; return true; @@ -2185,7 +2190,7 @@ namespace Barotrauma if (c.IsIncapacitated) { continue; } if (c.IsPlayer) { - if (c.SelectedConstruction == target.Item) + if (c.SelectedItem == target.Item) { // If the other character is player, don't try to operate other = c; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 0c46fca5d..5dc043af1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -79,7 +79,8 @@ namespace Barotrauma { pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true) { - GetNodePenalty = GetNodePenalty + GetNodePenalty = GetNodePenalty, + GetSingleNodePenalty = GetSingleNodePenalty }; this.canOpenDoors = canOpenDoors; @@ -360,7 +361,7 @@ namespace Barotrauma Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1); - if (useLadders && character.SelectedConstruction != ladders.Item) + if (useLadders && character.SelectedSecondaryItem != ladders.Item) { if (character.CanInteractWith(ladders.Item)) { @@ -372,7 +373,7 @@ namespace Barotrauma // Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder. // The intention of this code is to prevent the bots from dropping from the "double ladders". var previousLadders = currentPath.PrevNode?.Ladders; - if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item && + if (previousLadders != null && previousLadders != ladders && character.SelectedSecondaryItem != previousLadders.Item && character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5) { previousLadders.Item.TryInteract(character, forceSelectKey: true); @@ -382,8 +383,7 @@ namespace Barotrauma var collider = character.AnimController.Collider; if (character.IsClimbing && !useLadders) { - character.AnimController.Anim = AnimController.Animation.None; - character.SelectedConstruction = null; + character.StopClimbing(); } if (character.IsClimbing && useLadders) { @@ -402,15 +402,14 @@ namespace Barotrauma // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. bool isAboveFloor = heightFromFloor > -0.1f; // If the next waypoint is horizontally far, we don't want to keep holding the ladders - if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) + if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) { - character.AnimController.Anim = AnimController.Animation.None; - character.SelectedConstruction = null; + character.StopClimbing(); } else if (nextLadder != null && !nextLadderSameAsCurrent) { // Try to change the ladder (hatches between two submarines) - if (character.SelectedConstruction != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) + if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) { if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) { @@ -418,7 +417,7 @@ namespace Barotrauma } } } - if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10) + if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)) { NextNode(!doorsChecked); } @@ -528,7 +527,7 @@ namespace Barotrauma { // We'll want this to run each time, because the delegate is used to find a valid button component. bool canAccessButtons = false; - foreach (var button in door.Item.GetConnectedComponents(true)) + foreach (var button in door.Item.GetConnectedComponents(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state")) { if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) { @@ -676,6 +675,8 @@ namespace Barotrauma } } float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); + //heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them + if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; } if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item)) { closestButton = button; @@ -756,42 +757,8 @@ namespace Barotrauma private float? GetNodePenalty(PathNode node, PathNode nextNode) { if (character == null) { return 0.0f; } - if (nextNode.Waypoint.isObstructed) { return null; } - float penalty = 0.0f; - if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f) - { - var door = nextNode.Waypoint.ConnectedDoor; - if (door == null) - { - penalty = 100.0f; - } - else - { - if (!CanAccessDoor(door, button => - { - // Ignore buttons that are on the wrong side of the door - if (door.IsHorizontal) - { - if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y)) - { - return false; - } - } - else - { - if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X)) - { - return false; - } - } - return true; - })) - { - return null; - } - } - } - + float? penalty = GetSingleNodePenalty(nextNode); + if (penalty == null) { return null; } bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) @@ -839,6 +806,47 @@ namespace Barotrauma return penalty; } + private float? GetSingleNodePenalty(PathNode node) + { + if (node.Waypoint.isObstructed) { return null; } + if (node.IsBlocked()) { return null; } + float penalty = 0.0f; + if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f) + { + var door = node.Waypoint.ConnectedDoor; + if (door == null) + { + penalty = 100.0f; + } + else + { + if (!CanAccessDoor(door, button => + { + // Ignore buttons that are on the wrong side of the door + if (door.IsHorizontal) + { + if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y)) + { + return false; + } + } + else + { + if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X)) + { + return false; + } + } + return true; + })) + { + return null; + } + } + } + return penalty; + } + public static float smallRoomSize = 500; public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 594ba9045..f3a2a9c39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -14,13 +14,11 @@ namespace Barotrauma public readonly LanguageIdentifier Language; public readonly List Conversations; - public readonly Dictionary PersonalityTraits; public NPCConversationCollection(NPCConversationsFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) { Language = element.GetAttributeIdentifier("language", "English").ToLanguageIdentifier(); Conversations = new List(); - PersonalityTraits = new Dictionary(); foreach (var subElement in element.Elements()) { Identifier elemName = new Identifier(subElement.Name.LocalName); @@ -28,11 +26,6 @@ namespace Barotrauma { Conversations.Add(new NPCConversation(subElement)); } - else if (elemName == "PersonalityTrait") - { - var personalityTrait = new NPCPersonalityTrait(subElement); - PersonalityTraits.Add(personalityTrait.Name, personalityTrait); - } } } @@ -361,10 +354,13 @@ namespace Barotrauma private static float GetConversationProbability(NPCConversation conversation) { - int index = previousConversations.IndexOf(conversation); - if (index < 0) return 10.0f; + //prefer choosing conversations with more flags (= for more specific situations) when possible + float baseProbability = MathF.Pow(conversation.Flags.Count + 1, 2); - return 1.0f - 1.0f / (index + 1); + int index = previousConversations.IndexOf(conversation); + if (index < 0) { return baseProbability * 10.0f; } + + return baseProbability + 1.0f - 1.0f / (index + 1); } #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 1a14bd206..cb7523470 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -529,6 +530,15 @@ namespace Barotrauma } return canEquip; } + protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet identifiersOrTags) + { + if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; } + foreach (var identifier in identifiersOrTags) + { + if (item.HasTag(identifier)) { return true; } + } + return false; + } protected bool CanEquip(Item item) => CanEquip(character, item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index c3a21854d..c0c6f9c69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using FarseerPhysics.Dynamics; using static Barotrauma.AIObjectiveFindSafety; +using System.Collections.Immutable; namespace Barotrauma { @@ -512,6 +513,23 @@ namespace Barotrauma foreach (var weapon in weaponList) { float priority = weapon.CombatPriority; + if (weapon is RepairTool repairTool) + { + switch (repairTool.UsableIn) + { + case RepairTool.UseEnvironment.Air: + if (character.InWater) { continue; } + break; + case RepairTool.UseEnvironment.Water: + if (!character.InWater) { continue; } + break; + case RepairTool.UseEnvironment.None: + continue; + case RepairTool.UseEnvironment.Both: + default: + break; + } + } if (prioritizeMelee) { if (weapon is MeleeWeapon) @@ -895,11 +913,14 @@ namespace Barotrauma private void RemoveFollowTarget() { - if (arrestingRegistered) + if (followTargetObjective != null) { - followTargetObjective.Completed -= OnArrestTargetReached; + if (arrestingRegistered) + { + followTargetObjective.Completed -= OnArrestTargetReached; + } + RemoveSubObjective(ref followTargetObjective); } - RemoveSubObjective(ref followTargetObjective); arrestingRegistered = false; } @@ -950,7 +971,7 @@ namespace Barotrauma /// /// Seeks for more ammunition. Creates a new subobjective. /// - private void SeekAmmunition(Identifier[] ammunitionIdentifiers) + private void SeekAmmunition(ImmutableHashSet ammunitionIdentifiers) { retreatTarget = null; RemoveSubObjective(ref retreatObjective); @@ -985,7 +1006,7 @@ namespace Barotrauma HumanAIController.UnequipEmptyItems(Weapon); RelatedItem item = null; Item ammunition = null; - Identifier[] ammunitionIdentifiers = null; + ImmutableHashSet ammunitionIdentifiers = null; if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) @@ -1011,8 +1032,8 @@ namespace Barotrauma if (ammunitionIdentifiers != null) { // Try reload ammunition from inventory - bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); - ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); + static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); if (ammunition != null) { var container = Weapon.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index dea08ca34..8a5c606b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -1,6 +1,8 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -11,14 +13,14 @@ namespace Barotrauma public Func GetItemPriority; - public Identifier[] ignoredContainerIdentifiers; + public ImmutableHashSet ignoredContainerIdentifiers; public bool checkInventory = true; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates) private readonly bool spawnItemIfNotFound; //can either be a tag or an identifier - public readonly Identifier[] itemIdentifiers; + public readonly ImmutableHashSet itemIdentifiers; public readonly ItemContainer container; private readonly Item item; public Item ItemToContain { get; private set; } @@ -61,9 +63,9 @@ namespace Barotrauma } public AIObjectiveContainItem(Character character, Identifier itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new Identifier[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } + : this(character, itemIdentifier.ToEnumerable().ToImmutableHashSet(), container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveContainItem(Character character, Identifier[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveContainItem(Character character, ImmutableHashSet itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; @@ -102,7 +104,10 @@ namespace Barotrauma return containedItemCount >= ItemCount; } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && i.HasAccess(character); + private bool CheckItem(Item item) + { + return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); + } protected override void Act(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 047c1b636..a41303c20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -1,5 +1,7 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -13,7 +15,7 @@ namespace Barotrauma //can either be a tag or an identifier private readonly string[] itemIdentifiers; private readonly ItemContainer sourceContainer; - private ItemContainer targetContainer; + private readonly ItemContainer targetContainer; private readonly Item targetItem; private AIObjectiveGetItem getItemObjective; @@ -127,7 +129,7 @@ namespace Barotrauma RemoveExistingPredicate = RemoveExistingPredicate, RemoveMax = RemoveExistingMax, GetItemPriority = GetItemPriority, - ignoredContainerIdentifiers = sourceContainer != null ? new Identifier[] { sourceContainer.Item.Prefab.Identifier } : null + ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet() }, onCompleted: () => IsCompleted = true, onAbandon: () => Abandon = true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index a2567e72b..3e5531d20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -177,7 +177,7 @@ namespace Barotrauma TargetName = Leak.FlowTargetHull?.DisplayName, requiredCondition = () => Leak.Submarine == character.Submarine && - Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h), + Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 853abb82e..407b65230 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -21,10 +21,10 @@ namespace Barotrauma public float TargetCondition { get; set; } = 1; public bool AllowDangerousPressure { get; set; } - public readonly ImmutableArray IdentifiersOrTags; + public readonly ImmutableHashSet IdentifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) - private bool spawnItemIfNotFound = false; + private readonly bool spawnItemIfNotFound = false; private Item targetItem; private readonly Item originalTarget; @@ -32,8 +32,8 @@ namespace Barotrauma private bool isDoneSeeking; public Item TargetItem => targetItem; private int currSearchIndex; - public Identifier[] ignoredContainerIdentifiers; - public Identifier[] ignoredIdentifiersOrTags; + public ImmutableHashSet ignoredContainerIdentifiers; + public ImmutableHashSet ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; @@ -93,8 +93,8 @@ namespace Barotrauma Equip = equip; this.spawnItemIfNotFound = spawnItemIfNotFound; this.checkInventory = checkInventory; - IdentifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray(); - ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray(); + IdentifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableHashSet(); + ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet(); } public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) @@ -558,11 +558,11 @@ namespace Barotrauma { if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; - if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } + if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } - return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); + return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 0587b597f..e6d81bd12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -25,7 +25,7 @@ namespace Barotrauma public bool RequireAllItems { get; set; } private readonly ImmutableArray gearTags; - private readonly Identifier[] ignoredTags; + private readonly ImmutableHashSet ignoredTags; private bool subObjectivesCreated; public readonly HashSet achievedItems = new HashSet(); @@ -33,7 +33,7 @@ namespace Barotrauma public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray(); - ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray(); + ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet(); } protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 4bf65a912..d8607e729 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -196,9 +196,10 @@ namespace Barotrauma character.AIController.SteeringManager.Reset(); return; } - if (!character.IsClimbing) + character.SelectedItem = null; + if (character.SelectedSecondaryItem != null && !character.SelectedSecondaryItem.IsLadder) { - character.SelectedConstruction = null; + character.SelectedSecondaryItem = null; } if (Target is Entity e) { @@ -594,6 +595,10 @@ namespace Barotrauma { return c.CurrentHull; } + else if (target is Structure structure) + { + return Hull.FindHull(structure.Position, useWorldCoordinates: false); + } else if (target is Gap g) { return g.FlowTargetHull; @@ -647,7 +652,7 @@ namespace Barotrauma { if (character.IsClimbing) { - if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder) + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder && !PathSteering.CurrentPath.IsAtEndNode) { if (Target.WorldPosition.Y > character.WorldPosition.Y) { @@ -694,7 +699,7 @@ namespace Barotrauma { if (Target is Item item) { - if (!character.IsClimbing && character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; } + if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; } } else if (Target is Character targetCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 5b5665bf9..72afb8181 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -161,10 +161,7 @@ namespace Barotrauma character.DeselectCharacter(); } - if (!character.IsClimbing) - { - character.SelectedConstruction = null; - } + character.SelectedItem = null; CleanupItems(deltaTime); @@ -262,7 +259,8 @@ namespace Barotrauma // Check that there is no unsafe hulls on the way to the target if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } return true; - }, endNodeFilter: node => !isCurrentHullAllowed | !IsForbidden(node.Waypoint.CurrentHull)); + //don't stop at ladders when idling + }, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); if (path.Unreachable) { //can't go to this room, remove it from the list and try another room @@ -293,7 +291,9 @@ namespace Barotrauma } else if (currentTarget != null) { - PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); + PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, + nodeFilter: node => node.Waypoint.CurrentHull != null, + endNodeFilter: node => node.Waypoint.Ladders == null); } else { @@ -310,7 +310,7 @@ namespace Barotrauma if (character.AnimController.GetHeightFromFloor() < 0.1f) { character.AnimController.Anim = AnimController.Animation.None; - character.SelectedConstruction = null; + character.SelectedSecondaryItem = null; } return; } @@ -375,7 +375,7 @@ namespace Barotrauma } chairCheckTimer -= deltaTime; - if (chairCheckTimer <= 0.0f && character.SelectedConstruction == null) + if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null) { foreach (Item item in Item.ItemList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index cfd3e7ca9..b8c94aca0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -54,7 +54,7 @@ namespace Barotrauma if (ValidContainableItemIdentifiers.None()) { #if DEBUG - DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}"); + DebugConsole.LogError($"No valid containable item identifiers found for the Load Item objective targeting {Container}"); #endif Abandon = true; return; @@ -250,7 +250,7 @@ namespace Barotrauma catch (NotImplementedException) { #if DEBUG - DebugConsole.ShowError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty"); + DebugConsole.LogError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty"); #endif return 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index a5b9338dc..c54980f2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -90,7 +90,7 @@ namespace Barotrauma catch (NotImplementedException) { #if DEBUG - DebugConsole.ShowError($"Unexpected target condition \"{targetCondition}\" in AIObjectiveLoadItems.ItemMatchesTargetCondition"); + DebugConsole.LogError($"Unexpected target condition \"{targetCondition}\" in AIObjectiveLoadItems.ItemMatchesTargetCondition"); #endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index f1bf0e45f..aed77cb77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -37,6 +37,8 @@ namespace Barotrauma public Func completionCondition; private bool isDoneOperating; + public float? OverridePriority = null; + protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); @@ -52,7 +54,11 @@ namespace Barotrauma } else { - if (isOrder) + if (OverridePriority.HasValue) + { + Priority = OverridePriority.Value; + } + else if (isOrder) { Priority = objectiveManager.GetOrderPriority(this); } @@ -135,7 +141,7 @@ namespace Barotrauma float value = CumulatedDevotion + (max * PriorityModifier); Priority = MathHelper.Clamp(value, 0, max); } - else + else if (!OverridePriority.HasValue) { float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier); float max = AIObjectiveManager.LowestOrderPriority - 1; @@ -204,8 +210,15 @@ namespace Barotrauma { if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false)) { - HumanAIController.FaceTarget(target.Item); - if (character.SelectedConstruction != target.Item) + if (target.Item.GetComponent() is not Controller { ControlCharacterPose: true }) + { + HumanAIController.FaceTarget(target.Item); + } + else + { + HumanAIController.SteeringManager.Reset(); + } + if (character.SelectedItem != target.Item && character.SelectedSecondaryItem != target.Item) { target.Item.TryInteract(character, forceSelectKey: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index ee2c985fb..78fc1a261 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -26,7 +26,7 @@ namespace Barotrauma private bool IsRepairing() => IsRepairing(character, Item); private readonly bool isPriority; - public static bool IsRepairing(Character character, Item item) => character.SelectedConstruction == item && item.Repairables.Any(r => r.CurrentFixer == character); + public static bool IsRepairing(Character character, Item item) => character.SelectedItem == item && item.Repairables.Any(r => r.CurrentFixer == character); public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base(character, objectiveManager, priorityModifier) @@ -165,7 +165,7 @@ namespace Barotrauma return; } } - if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false)) + if (character.CanInteractWith(Item, out _, checkLinked: false)) { waitTimer += deltaTime; if (waitTimer < WaitTimeBeforeRepair) { return; } @@ -184,12 +184,12 @@ namespace Barotrauma } if (!Abandon) { - if (character.SelectedConstruction != Item) + if (character.SelectedItem != Item) { if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) || Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true)) { - character.SelectedConstruction = Item; + character.SelectedItem = Item; } else { @@ -232,8 +232,6 @@ namespace Barotrauma previousCondition = -1; var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { - // Don't stop in ladders, because we can't interact with other items while holding the ladders. - endNodeFilter = node => node.Waypoint.Ladders == null, TargetName = Item.Name }; if (repairTool != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 90578f6b9..c2539d899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -67,7 +67,7 @@ namespace Barotrauma if (!ViableForRepair(item, character, HumanAIController)) { return false; }; if (!Objectives.ContainsKey(item)) { - if (item != character.SelectedConstruction) + if (item != character.SelectedItem) { if (NearlyFullCondition(item)) { return false; } } @@ -96,7 +96,7 @@ namespace Barotrauma protected override float TargetEvaluation() { - var selectedItem = character.SelectedConstruction; + var selectedItem = character.SelectedItem; if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100) { // Don't stop fixing until completely done diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index c700ff0ad..30d687a2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -278,7 +278,7 @@ namespace Barotrauma float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f; //find which treatments are the most suitable to treat the character's current condition - targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false, predictFutureDuration: 10.0f); + targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index b9c716c78..042e913cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -3,9 +3,8 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -151,7 +150,7 @@ namespace Barotrauma public OrderPrefab(ContentXElement orderElement, OrdersFile file) : base(file, orderElement.GetAttributeIdentifier("identifier", "")) { Name = TextManager.Get($"OrderName.{Identifier}"); - ContextualName = TextManager.Get($"OrderNameContextual.{Identifier}"); + ContextualName = TextManager.Get($"OrderNameContextual.{Identifier}").Fallback(Name); string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); if (!string.IsNullOrWhiteSpace(targetItemType)) @@ -435,7 +434,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); return null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 3a8186c25..9e9398673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -109,6 +109,8 @@ namespace Barotrauma { public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode); public GetNodePenaltyHandler GetNodePenalty; + public delegate float? GetSingleNodePenaltyHandler(PathNode node); + public GetSingleNodePenaltyHandler GetSingleNodePenalty; private readonly List nodes; private readonly bool isCharacter; @@ -282,8 +284,6 @@ namespace Barotrauma } //avoid stopping at a doorway if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; } - //avoid stopping at a ladder - if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; } } //optimization: node extremely far (> 100m / 800 m) from the end position, don't try to use it as an end node if (node.TempDistance > (InsideSubmarine ? 100.0f * 100.0f : 800.0f * 800.0f)) @@ -325,15 +325,24 @@ namespace Barotrauma #endif return new SteeringPath(true); } - var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); - return path; + return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); - bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true) + bool IsValidStartNode(PathNode node) => IsValidNode(node, (isCharacter, start), startNodeFilter); + + bool IsValidEndNode(PathNode node) => IsValidNode(node, (isCharacter && checkVisibility, end), endNodeFilter); + + bool IsValidNode(PathNode node, (bool check, Vector2 start) visibilityCheck, Func extraFilter) { - //if searching for a path inside the sub, make sure the waypoint is visible - if (checkVisibility && isCharacter) + if (nodeFilter != null && !nodeFilter(node)) { return false; } + if (extraFilter != null && !extraFilter(node)) { return false; } + if (GetSingleNodePenalty != null && GetSingleNodePenalty(node) == null) { return false; } + if (node.Waypoint.ConnectedGap != null) { - var body = Submarine.PickBody(rayStart, node.TempPosition, + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; } + } + if (visibilityCheck.check) + { + var body = Submarine.PickBody(visibilityCheck.start, node.TempPosition, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); if (body != null) { @@ -344,36 +353,6 @@ namespace Barotrauma } return true; } - - bool IsValidStartNode(PathNode node) - { - if (nodeFilter != null && !nodeFilter(node)) { return false; } - if (startNodeFilter != null && !startNodeFilter(node)) { return false; } - if (node.Waypoint.isObstructed) { return false; } - // Always check the visibility for the start node - if (!IsWaypointVisible(node, start)) { return false; } - if (node.IsBlocked()) { return false; } - if (node.Waypoint.ConnectedGap != null) - { - if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; } - } - return true; - } - - bool IsValidEndNode(PathNode node) - { - if (nodeFilter != null && !nodeFilter(node)) { return false; } - if (endNodeFilter != null && !endNodeFilter(node)) { return false; } - if (node.Waypoint.isObstructed) { return false; } - // Only check the visibility for the end node when allowed (fix leaks) - if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; } - if (node.IsBlocked()) { return false; } - if (node.Waypoint.ConnectedGap != null) - { - if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; } - } - return true; - } } private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0) @@ -402,15 +381,13 @@ namespace Barotrauma foreach (PathNode node in nodes) { if (node.state != 1 || node.F > dist) { continue; } - if (isCharacter && node.Waypoint.isObstructed) { continue; } if (filter != null && !filter(node)) { continue; } - if (node.IsBlocked()) { continue; } if (node.Waypoint.ConnectedGap != null) { if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } - } + } dist = node.F; - currNode = node; + currNode = node; } if (currNode == null || currNode == end) { break; } @@ -515,7 +492,4 @@ namespace Barotrauma private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth; } -} - - - +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs index 24a3feb2b..eb7b56db4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs @@ -115,6 +115,8 @@ namespace Barotrauma } } + public bool IsAtEndNode => currentIndex >= nodes.Count - 1; + public List Nodes { get { return nodes; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 25d23300b..ceeeab73b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -444,7 +444,7 @@ namespace Barotrauma #if SERVER public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null) { - msg.Write(IsAlive); + msg.WriteBoolean(IsAlive); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 258eb4e33..928dd8e74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -101,7 +101,7 @@ namespace Barotrauma { if (InWater || !CanWalk) { - return TargetMovement.LengthSquared() > MathUtils.Pow2(SwimSlowParams.MovementSpeed); + return TargetMovement.LengthSquared() > MathUtils.Pow2(SwimSlowParams.MovementSpeed + 0.0001f); } else { @@ -134,9 +134,12 @@ namespace Barotrauma } } - public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR }; + public enum Animation { None, Climbing, UsingItem, Struggle, CPR, UsingItemWhileClimbing }; public Animation Anim; + public bool IsUsingItem => Anim == Animation.UsingItem || Anim == Animation.UsingItemWhileClimbing; + public bool IsClimbing => Anim == Animation.Climbing || Anim == Animation.UsingItemWhileClimbing; + public Vector2 AimSourceWorldPos { get @@ -280,7 +283,7 @@ namespace Barotrauma public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { useItemTimer = 0.5f; - Anim = Animation.UsingConstruction; + StartUsingItem(); if (!allowMovement) { @@ -359,8 +362,13 @@ namespace Barotrauma Vector2 itemPos = aim ? aimPos : holdPos; - var controller = character.SelectedConstruction?.GetComponent(); + var controller = character.SelectedItem?.GetComponent(); bool usingController = controller != null && !controller.AllowAiming; + if (!usingController) + { + controller = character.SelectedSecondaryItem?.GetComponent(); + usingController = controller != null && !controller.AllowAiming; + } bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; float itemAngle; Holdable holdable = item.GetComponent(); @@ -722,5 +730,45 @@ namespace Barotrauma CalculateArmLengths(); } } + + private void StartAnimation(Animation animation) + { + if (animation == Animation.UsingItem) + { + Anim = IsClimbing ? Animation.UsingItemWhileClimbing : Animation.UsingItem; + } + else if (animation == Animation.Climbing) + { + Anim = IsUsingItem ? Animation.UsingItemWhileClimbing : Animation.Climbing; + } + else + { + Anim = animation; + } + } + + private void StopAnimation(Animation animation) + { + if (animation == Animation.UsingItem) + { + Anim = IsClimbing ? Animation.Climbing : Animation.None; + } + else if (animation == Animation.Climbing) + { + Anim = IsUsingItem ? Animation.UsingItem : Animation.None; + } + else + { + Anim = Animation.None; + } + } + + public void StartUsingItem() => StartAnimation(Animation.UsingItem); + + public void StartClimbing() => StartAnimation(Animation.Climbing); + + public void StopUsingItem() => StopAnimation(Animation.UsingItem); + + public void StopClimbing() => StopAnimation(Animation.Climbing); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 188cc7aaa..8aaa57eb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -237,13 +237,14 @@ namespace Barotrauma public override void UpdateAnim(float deltaTime) { - if (Frozen) return; + if (Frozen) { return; } if (MainLimb == null) { return; } levitatingCollider = !IsHanging; ColliderIndex = Crouching && !swimming ? 1 : 0; - if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false || - character.SelectedConstruction?.GetComponent() != null || + if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || + (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || + character.SelectedSecondaryItem?.GetComponent() != null || (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; @@ -330,35 +331,41 @@ namespace Barotrauma Collider.SetTransform(Collider.SimPosition, Collider.Rotation + angleDiff); } } - - if (character.LockHands) - { - var leftHand = GetLimb(LimbType.LeftHand); - var rightHand = GetLimb(LimbType.RightHand); - - var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); - - rightHand.Disabled = true; - leftHand.Disabled = true; - - Vector2 midPos = waist.SimPosition; - Matrix torsoTransform = Matrix.CreateRotationZ(waist.Rotation); - - midPos += Vector2.Transform(new Vector2(-0.3f * Dir, -0.2f), torsoTransform); - - if (rightHand.PullJointEnabled) midPos = (midPos + rightHand.PullJointWorldAnchorB) / 2.0f; - HandIK(rightHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); - HandIK(leftHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); - } - else if (character.AnimController.AnimationTestPose) + + if (character.AnimController.AnimationTestPose) { ApplyTestPose(); } - else + else if (character.SelectedBy == null) { - if (Anim != Animation.UsingConstruction) + if (character.LockHands) { - ResetPullJoints(); + var leftHand = GetLimb(LimbType.LeftHand); + var rightHand = GetLimb(LimbType.RightHand); + + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + + rightHand.Disabled = true; + leftHand.Disabled = true; + + Vector2 midPos = waist.SimPosition; + Matrix torsoTransform = Matrix.CreateRotationZ(waist.Rotation); + + midPos += Vector2.Transform(new Vector2(-0.3f * Dir, -0.2f), torsoTransform); + if (rightHand.PullJointEnabled) midPos = (midPos + rightHand.PullJointWorldAnchorB) / 2.0f; + HandIK(rightHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); + HandIK(leftHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); + } + if (Anim != Animation.UsingItem) + { + if (Anim != Animation.UsingItemWhileClimbing) + { + ResetPullJoints(); + } + else + { + ResetPullJoints(l => l.IsLowerBody); + } } } @@ -377,49 +384,53 @@ namespace Barotrauma switch (Anim) { case Animation.Climbing: + case Animation.UsingItemWhileClimbing: levitatingCollider = false; UpdateClimbing(); + UpdateUseItemTimer(); break; case Animation.CPR: UpdateCPR(deltaTime); break; - case Animation.UsingConstruction: + case Animation.UsingItem: default: - if (Anim == Animation.UsingConstruction) - { - useItemTimer -= deltaTime; - if (useItemTimer <= 0.0f) Anim = Animation.None; - } - + UpdateUseItemTimer(); swimmingStateLockTimer -= deltaTime; - if (forceStanding || character.AnimController.AnimationTestPose) { swimming = false; } - else + else if (swimming != inWater && swimmingStateLockTimer <= 0.0f) { //0.5 second delay for switching between swimming and walking //prevents rapid switches between swimming/walking if the water level is fluctuating around the minimum swimming depth - if (swimming != inWater && swimmingStateLockTimer <= 0.0f) - { - swimming = inWater; - swimmingStateLockTimer = 0.5f; - } + swimming = inWater; + swimmingStateLockTimer = 0.5f; } - if (swimming) { UpdateSwimming(); } - else + else if (character.SelectedItem == null || !(character.SelectedSecondaryItem?.GetComponent() is { } controller) || + !controller.ControlCharacterPose || !controller.UserInCorrectPosition) { UpdateStanding(); } - break; } + void UpdateUseItemTimer() + { + if (IsUsingItem) + { + useItemTimer -= deltaTime; + if (useItemTimer <= 0.0f) + { + StopUsingItem(); + } + } + } + if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) { Flip(); @@ -841,7 +852,9 @@ namespace Barotrauma float targetSpeed = TargetMovement.Length(); if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !Aiming) { - if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) + if (!IsUsingItem && + !(character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) && + !(character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false)) { if (rotation > 20 && rotation < 170) { @@ -1041,12 +1054,17 @@ namespace Barotrauma void UpdateClimbing() { - var ladder = character.SelectedConstruction?.GetComponent(); - if (ladder == null || character.IsIncapacitated) + var ladder = character.SelectedSecondaryItem?.GetComponent(); + if (character.IsIncapacitated) { Anim = Animation.None; return; } + else if (ladder == null) + { + StopClimbing(); + return; + } onGround = false; IgnorePlatforms = true; @@ -1209,15 +1227,21 @@ namespace Barotrauma { RotateHead(head); } + else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem) + { + Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir; + float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir); + head.body.SmoothRotate(targetRotation, force: WalkParams.HeadTorque); + } else { float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; - head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); + head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: WalkParams.HeadTorque); } - if (!ladder.Item.Prefab.Triggers.Any()) + if (ladder.Item.Prefab.Triggers.None()) { - character.SelectedConstruction = null; + character.SelectedSecondaryItem = null; return; } @@ -1247,8 +1271,7 @@ namespace Barotrauma if (!isClimbing) { - Anim = Animation.None; - character.SelectedConstruction = null; + character.StopClimbing(); IgnorePlatforms = false; } @@ -1474,17 +1497,17 @@ namespace Barotrauma public override void DragCharacter(Character target, float deltaTime) { - if (target == null) return; + if (target == null) { return; } Limb torso = GetLimb(LimbType.Torso); Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftHand); + Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb; - Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightHand); + Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso); if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb; @@ -1493,7 +1516,7 @@ namespace Barotrauma target.AnimController.ResetPullJoints(); } - if (Anim == Animation.Climbing) + if (IsClimbing) { //cannot drag up ladders if the character is conscious if (target.AllowInput && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)) @@ -1613,7 +1636,7 @@ namespace Barotrauma } //only pull with one hand when swimming - if (i > 0 && inWater) continue; + if (i > 0 && inWater) { continue; } Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - pullLimb.WorldPosition); @@ -1665,14 +1688,15 @@ namespace Barotrauma targetForce = 5000.0f; } - if (!target.AllowInput) - { - targetLimb.PullJointEnabled = true; - targetLimb.PullJointMaxForce = targetForce; - targetLimb.PullJointWorldAnchorB = targetAnchor; - } + targetLimb.PullJointEnabled = true; + targetLimb.PullJointMaxForce = targetForce; + targetLimb.PullJointWorldAnchorB = targetAnchor; + targetLimb.Disabled = true; - target.AnimController.movement = -diff; + if (diff.LengthSquared() > 0.1f) + { + target.AnimController.movement = -diff; + } } float dist = ConvertUnits.ToSimUnits(Vector2.Distance(target.WorldPosition, WorldPosition)); @@ -1698,8 +1722,17 @@ namespace Barotrauma } else if (target is AICharacter && target != Character.Controlled) { - target.AnimController.TargetDir = WorldPosition.X > target.WorldPosition.X ? Direction.Right : Direction.Left; - target.AnimController.TargetMovement = (character.SimPosition + Vector2.UnitX * Dir) - target.SimPosition; + if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X) + { + target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + } + else + { + target.AnimController.TargetDir = WorldPosition.X > target.WorldPosition.X ? Direction.Right : Direction.Left; + } + //make the target stand 0.5 meters away from this character, on the side they're currently at + Vector2 movement = (character.SimPosition + Vector2.UnitX * 0.5f * Math.Sign(target.SimPosition.X - character.SimPosition.X)) - target.SimPosition; + target.AnimController.TargetMovement = movement.LengthSquared() > 0.01f ? movement : Vector2.Zero; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 7f0248822..c02ab109a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -930,12 +930,13 @@ namespace Barotrauma { limb.MoveToPos(pos, amount, pullFromCenter); } - - public void ResetPullJoints() + + public void ResetPullJoints(Func condition = null) { for (int i = 0; i < Limbs.Length; i++) { if (Limbs[i] == null) { continue; } + if (condition != null && !condition(Limbs[i])) { continue; } Limbs[i].PullJointEnabled = false; } } @@ -1241,7 +1242,7 @@ namespace Barotrauma { //find the room which the limb is in //the room where the ragdoll is in is used as the "guess", meaning that it's checked first - Hull limbHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull); + Hull newHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull); bool prevInWater = limb.InWater; limb.InWater = false; @@ -1250,38 +1251,37 @@ namespace Barotrauma { limb.InWater = false; } - else if (limbHull == null) + else if (newHull == null) { //limb isn't in any room -> it's in the water limb.InWater = true; - if (limb.type == LimbType.Head) headInWater = true; + if (limb.type == LimbType.Head) { headInWater = true; } } - else if (limbHull.WaterVolume > 0.0f && Submarine.RectContains(limbHull.Rect, limb.Position)) + else if (newHull.WaterVolume > 0.0f && Submarine.RectContains(newHull.Rect, limb.Position)) { - if (limb.Position.Y < limbHull.Surface) + if (limb.Position.Y < newHull.Surface) { limb.InWater = true; - surfaceY = limbHull.Surface; + surfaceY = newHull.Surface; if (limb.type == LimbType.Head) { headInWater = true; } } //the limb has gone through the surface of the water - if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater) + if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater && newHull == limb.Hull) { - Splash(limb, limbHull); - + Splash(limb, newHull); //if the Character dropped into water, create a wave if (limb.LinearVelocity.Y < 0.0f) { Vector2 impulse = limb.LinearVelocity * limb.Mass; - int n = (int)((limb.Position.X - limbHull.Rect.X) / Hull.WaveWidth); - limbHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f); + int n = (int)((limb.Position.X - newHull.Rect.X) / Hull.WaveWidth); + newHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f); } } } - + limb.Hull = newHull; limb.Update(deltaTime); } @@ -1499,12 +1499,12 @@ namespace Barotrauma } if (flowForce.LengthSquared() > 0.001f) - { - Collider.ApplyForce(flowForce); + { + Collider.ApplyForce(flowForce * (Collider.Mass / Mass)); foreach (Limb limb in limbs) { if (!limb.InWater) { continue; } - limb.body.ApplyForce(flowForce); + limb.body.ApplyForce(flowForce * (limb.Mass / Mass * limbs.Length)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index d47bbf405..d5d02d2d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -100,6 +100,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Should the rope attached to this limb snap upon choosing a new attack?"), Editable] + public bool SnapRopeOnNewAttack { get; private set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] public bool Retreat { get; private set; } @@ -309,7 +312,7 @@ namespace Barotrauma List multipliedAfflictions = new List(); foreach (Affliction affliction in Afflictions.Keys) { - multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier)); + multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability)); } return multipliedAfflictions; } @@ -399,9 +402,8 @@ namespace Barotrauma } else { - string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; - if (afflictionPrefab == null) + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab)) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); continue; @@ -427,15 +429,13 @@ namespace Barotrauma Afflictions.Clear(); foreach (var subElement in element.GetChildElements("affliction")) { - AfflictionPrefab afflictionPrefab; Affliction affliction; Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab)) { DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\"."); continue; } - afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index b2d7b1192..cb2960be4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -43,7 +43,7 @@ namespace Barotrauma } set { - if (value == enabled) return; + if (value == enabled) { return; } if (Removed) { @@ -66,6 +66,32 @@ namespace Barotrauma } } + + private bool disabledByEvent; + /// + /// MonsterEvents disable monsters (which includes removing them from the character list, so they essentially "don't exist") until they're ready to spawn + /// + public bool DisabledByEvent + { + get { return disabledByEvent; } + set + { + if (value == disabledByEvent) { return; } + disabledByEvent = value; + if (disabledByEvent) + { + Enabled = false; + CharacterList.Remove(this); + if (AiTarget != null) { AITarget.List.Remove(AiTarget); } + } + else + { + if (!CharacterList.Contains(this)) { CharacterList.Add(this); } + if (AiTarget != null && !AITarget.List.Contains(AiTarget)) { AITarget.List.Add(AiTarget); } + } + } + } + public Hull PreviousHull = null; public Hull CurrentHull = null; @@ -526,6 +552,10 @@ namespace Barotrauma set { lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); + if (value) + { + SelectedCharacter = null; + } #if CLIENT HintManager.OnHandcuffed(this); #endif @@ -576,13 +606,10 @@ namespace Barotrauma get { return selectedCharacter; } set { - if (value == selectedCharacter) return; - if (selectedCharacter != null) - selectedCharacter.selectedBy = null; + if (value == selectedCharacter) { return; } + if (selectedCharacter != null) { selectedCharacter.selectedBy = null; } selectedCharacter = value; - if (selectedCharacter != null) - selectedCharacter.selectedBy = this; - + if (selectedCharacter != null) {selectedCharacter.selectedBy = this; } #if CLIENT CharacterHealth.SetHealthBarVisibility(value == null); #endif @@ -684,6 +711,14 @@ namespace Barotrauma get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; } } + /// + /// Can be used by status effects + /// + public AnimController.Animation Anim + { + get { return AnimController?.Anim ?? AnimController.Animation.None; } + } + public const float KnockbackCooldown = 5.0f; public float KnockbackCooldownTimer; @@ -816,43 +851,65 @@ namespace Barotrauma get { return AnimController?.Collider?.LinearVelocity.Length() ?? 0.0f; } } - private Item _selectedConstruction; - public Item SelectedConstruction + private Item _selectedItem; + /// + /// The primary selected item. It can be any device that character interacts with. This excludes items like ladders and chairs which are assigned to . + /// + public Item SelectedItem { - get => _selectedConstruction; + get => _selectedItem; set { - var prevSelectedConstruction = _selectedConstruction; - _selectedConstruction = value; + var prevSelectedItem = _selectedItem; + _selectedItem = value; #if CLIENT - HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction); + HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem); if (Controlled == this) { - if (_selectedConstruction == null) + if (_selectedItem == null) { GameMain.GameSession?.CrewManager?.ResetCrewList(); } - else if (_selectedConstruction.GetComponent() == null) + else if (!_selectedItem.IsLadder) { GameMain.GameSession?.CrewManager?.AutoHideCrewList(); } } #endif - if (prevSelectedConstruction == null && _selectedConstruction != null) + if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0) + { + double selectedDuration = Timing.TotalTime - itemSelectedTime; + if (itemSelectedDurations.ContainsKey(prevSelectedItem.Prefab)) + { + itemSelectedDurations[prevSelectedItem.Prefab] += selectedDuration; + } + else + { + itemSelectedDurations.Add(prevSelectedItem.Prefab, selectedDuration); + } + itemSelectedTime = 0; + } + if (_selectedItem != null && (prevSelectedItem == null || prevSelectedItem != _selectedItem)) { itemSelectedTime = Timing.TotalTime; } - else if (prevSelectedConstruction != null && _selectedConstruction == null && itemSelectedTime > 0) - { - if (!itemSelectedDurations.ContainsKey(prevSelectedConstruction.Prefab)) - { - itemSelectedDurations.Add(prevSelectedConstruction.Prefab, 0); - } - itemSelectedDurations[prevSelectedConstruction.Prefab] += Timing.TotalTime - itemSelectedTime; - itemSelectedTime = 0; - } } } + /// + /// The secondary selected item. It's an item other than a device (see ), e.g. a ladder or a chair. + /// + public Item SelectedSecondaryItem { get; set; } + /// + /// Has the characters selected a primary or a secondary item? + /// + public bool HasSelectedAnyItem => SelectedItem != null || SelectedSecondaryItem != null; + /// + /// Is the item either the primary or the secondary selected item? + /// + /// + /// + public bool IsAnySelectedItem(Item item) => item == SelectedItem || item == SelectedSecondaryItem; + public bool HasSelectedAnotherSecondaryItem(Item item) => SelectedSecondaryItem != null && SelectedSecondaryItem != item; public Item FocusedItem { @@ -941,7 +998,7 @@ namespace Barotrauma { get { - return SelectedConstruction == null || SelectedConstruction.GetComponent() != null || (SelectedConstruction.GetComponent()?.AllowAiming ?? false); + return SelectedItem == null || (SelectedItem.GetComponent()?.AllowAiming ?? false); } } @@ -1231,23 +1288,30 @@ namespace Barotrauma if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk") { - // Get the non husked name and find the ragdoll with it - var matchingAffliction = AfflictionPrefab.List - .Where(p => p is AfflictionPrefabHusk) - .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Any(t => t == AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p))); Identifier nonHuskedSpeciesName = Identifier.Empty; - if (matchingAffliction == null) + AfflictionPrefabHusk matchingAffliction = null; + foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType()) { - DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!"); + var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab); + if (huskPrefab.TargetSpecies.Contains(nonHuskedName)) + { + var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab); + if (huskedSpeciesName.Equals(speciesName)) + { + nonHuskedSpeciesName = nonHuskedName; + matchingAffliction = huskPrefab; + break; + } + } + } + if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty) + { + DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n" + + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\""); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); speciesName = nonHuskedSpeciesName; } - else - { - nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction); - } if (ragdollParams == null && prefab.VariantOf == null) { Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; @@ -1487,9 +1551,20 @@ namespace Barotrauma public void GiveJobItems(WayPoint spawnPoint = null) { - if (info?.Job == null) { return; } - info.Job.GiveJobItems(this, spawnPoint); - + if (info == null) { return; } + if (info.HumanPrefabIds != default) + { + var humanPrefab = NPCSet.Get(info.HumanPrefabIds.NpcSetIdentifier, info.HumanPrefabIds.NpcIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); + } + else if (humanPrefab.GiveItems(this, Submarine)) + { + return; + } + } + info.Job?.GiveJobItems(this, spawnPoint); GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint); } @@ -1532,10 +1607,17 @@ namespace Barotrauma if (item?.GetComponent() is Wearable wearable && !Inventory.IsInLimbSlot(item, InvSlotType.Any)) { - if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) + foreach (var allowedSlot in wearable.AllowedSlots) { - skillLevel += skillValue; + if (allowedSlot == InvSlotType.Any) { continue; } + if (!Inventory.IsInLimbSlot(item, allowedSlot)) { continue; } + if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) + { + skillLevel += skillValue; + break; + } } + } } } @@ -1549,7 +1631,7 @@ namespace Barotrauma public Vector2? OverrideMovement { get; set; } public bool ForceRun { get; set; } - public bool IsClimbing => AnimController.Anim == AnimController.Animation.Climbing; + public bool IsClimbing => AnimController.IsClimbing; public Vector2 GetTargetMovement() { @@ -1779,6 +1861,11 @@ namespace Barotrauma return speed; } + /// + /// Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping. + /// + private const float cursorFollowMargin = 40; + public void Control(float deltaTime, Camera cam) { ViewTarget = null; @@ -1811,10 +1898,10 @@ namespace Barotrauma } if (!aiControlled && - AnimController.Anim != AnimController.Animation.UsingConstruction && + !AnimController.IsUsingItem && AnimController.Anim != AnimController.Animation.CPR && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) && - AnimController.OnGround && !AnimController.InWater) + (AnimController.OnGround || IsClimbing) && !AnimController.InWater) { if (dontFollowCursor) { @@ -1822,13 +1909,11 @@ namespace Barotrauma } else { - // Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping. - float followMargin = 40; - if (CursorPosition.X < AnimController.Collider.Position.X - followMargin) + if (CursorPosition.X < AnimController.Collider.Position.X - cursorFollowMargin) { AnimController.TargetDir = Direction.Left; } - else if (CursorPosition.X > AnimController.Collider.Position.X + followMargin) + else if (CursorPosition.X > AnimController.Collider.Position.X + cursorFollowMargin) { AnimController.TargetDir = Direction.Right; } @@ -1975,7 +2060,8 @@ namespace Barotrauma } } - if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected) + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; + if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { foreach (Item item in HeldItems) { @@ -2006,24 +2092,24 @@ namespace Barotrauma } } - if (SelectedConstruction != null) + if (SelectedItem != null) { - if (IsKeyDown(InputType.Aim) || !SelectedConstruction.RequireAimToSecondaryUse) + if (IsKeyDown(InputType.Aim) || !SelectedItem.RequireAimToSecondaryUse) { - SelectedConstruction.SecondaryUse(deltaTime, this); + SelectedItem.SecondaryUse(deltaTime, this); } - if (IsKeyDown(InputType.Use) && SelectedConstruction != null && !SelectedConstruction.IsShootable) + if (IsKeyDown(InputType.Use) && SelectedItem != null && !SelectedItem.IsShootable) { - if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim)) + if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) { - SelectedConstruction.Use(deltaTime, this); + SelectedItem.Use(deltaTime, this); } } - if (IsKeyDown(InputType.Shoot) && SelectedConstruction != null && SelectedConstruction.IsShootable) + if (IsKeyDown(InputType.Shoot) && SelectedItem != null && SelectedItem.IsShootable) { - if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim)) + if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) { - SelectedConstruction.Use(deltaTime, this); + SelectedItem.Use(deltaTime, this); } } } @@ -2359,15 +2445,15 @@ namespace Barotrauma //wires are interactable if the character has selected an item the wire is connected to, //and it's disconnected from the other end - if (wire.Connections[0]?.Item != null && SelectedConstruction == wire.Connections[0].Item) + if (wire.Connections[0]?.Item != null && SelectedItem == wire.Connections[0].Item) { return wire.Connections[1] == null; } - if (wire.Connections[1]?.Item != null && SelectedConstruction == wire.Connections[1].Item) + if (wire.Connections[1]?.Item != null && SelectedItem == wire.Connections[1].Item) { return wire.Connections[0] == null; } - if (SelectedConstruction?.GetComponent()?.DisconnectedWires.Contains(wire) ?? false) + if (SelectedItem?.GetComponent()?.DisconnectedWires.Contains(wire) ?? false) { return wire.Connections[0] == null && wire.Connections[1] == null; } @@ -2393,7 +2479,7 @@ namespace Barotrauma Pickable pickableComponent = item.GetComponent(); if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; } - if (SelectedConstruction?.GetComponent()?.TargetItem == item) { return true; } + if (SelectedItem?.GetComponent()?.TargetItem == item) { return true; } //optimization: don't use HeldItems because it allocates memory and this method is executed very frequently var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); if (heldItem1?.GetComponent()?.TargetItem == item) { return true; } @@ -2436,27 +2522,43 @@ namespace Barotrauma distanceToItem = Vector2.Distance(rectIntersectionPoint, playerDistanceCheckPosition); } - if (distanceToItem > item.InteractDistance && item.InteractDistance > 0.0f) { return false; } + float interactDistance = item.InteractDistance; + if ((SelectedSecondaryItem != null || item.IsSecondaryItem) && AnimController is HumanoidAnimController c) + { + // Use a distance slightly shorter than the arms length to keep the character in a comfortable pose + float armLength = 0.75f * ConvertUnits.ToDisplayUnits(c.ArmLength); + interactDistance = Math.Min(interactDistance, armLength); + } + if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; } + + Vector2 itemPosition = item.SimPosition; + if (Submarine == null && item.Submarine != null) + { + //character is outside, item inside + itemPosition += item.Submarine.SimPosition; + } + else if (Submarine != null && item.Submarine == null) + { + //character is inside, item outside + itemPosition -= Submarine.SimPosition; + } + else if (Submarine != item.Submarine) + { + //character and the item are inside different subs + itemPosition += item.Submarine.SimPosition; + itemPosition -= Submarine.SimPosition; + } + + if (SelectedSecondaryItem != null && !item.IsSecondaryItem) + { + if (item.GetComponent() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; } + float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin); + if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; } + if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; } + } if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) { - Vector2 itemPosition = item.SimPosition; - if (Submarine == null && item.Submarine != null) - { - //character is outside, item inside - itemPosition += item.Submarine.SimPosition; - } - else if (Submarine != null && item.Submarine == null) - { - //character is inside, item outside - itemPosition -= Submarine.SimPosition; - } - else if (Submarine != item.Submarine) - { - //character and the item are inside different subs - itemPosition += item.Submarine.SimPosition; - itemPosition -= Submarine.SimPosition; - } var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { @@ -2529,12 +2631,12 @@ namespace Barotrauma if (!CanInteract) { - SelectedConstruction = null; + SelectedItem = SelectedSecondaryItem = null; focusedItem = null; if (!AllowInput) { FocusedCharacter = null; - if (SelectedCharacter != null) DeselectCharacter(); + if (SelectedCharacter != null) { DeselectCharacter(); } return; } } @@ -2578,8 +2680,8 @@ namespace Barotrauma AnimController.InWater : head.InWater; //climb ladders automatically when pressing up/down inside their trigger area - Ladder currentLadder = SelectedConstruction?.GetComponent(); - if ((SelectedConstruction == null || currentLadder != null) && + Ladder currentLadder = SelectedSecondaryItem?.GetComponent(); + if ((SelectedSecondaryItem == null || currentLadder != null) && !headInWater && Screen.Selected != GameMain.SubEditorScreen) { bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down); @@ -2621,7 +2723,7 @@ namespace Barotrauma { if (nearbyLadder.Select(this)) { - SelectedConstruction = nearbyLadder.Item; + SelectedSecondaryItem = nearbyLadder.Item; } } } @@ -2665,16 +2767,23 @@ namespace Barotrauma { FocusedCharacter.onCustomInteract(FocusedCharacter, this); } - else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent() == null) + else if (IsKeyHit(InputType.Deselect) && SelectedItem != null) { - SelectedConstruction = null; + SelectedItem = null; #if CLIENT CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && SelectedConstruction != null && SelectedConstruction.GetComponent() == null) + else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null) { - SelectedConstruction = null; + SelectedSecondaryItem = null; +#if CLIENT + CharacterHealth.OpenHealthWindow = null; +#endif + } + else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) + { + SelectedItem = SelectedSecondaryItem = null; } else if (focusedItem != null) { @@ -2890,7 +2999,7 @@ namespace Barotrauma } else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && - WorldPosition.Y < CharacterHealth.CrushDepth) + WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { //implode if below crush depth, and either outside or in a high-pressure hull if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) @@ -2923,7 +3032,7 @@ namespace Barotrauma { Stun = Math.Max(5.0f, Stun); AnimController.ResetPullJoints(); - SelectedConstruction = null; + SelectedItem = SelectedSecondaryItem = null; return; } @@ -2980,7 +3089,7 @@ namespace Barotrauma humanAnimController.Crouching = false; } AnimController.ResetPullJoints(); - SelectedConstruction = null; + SelectedItem = SelectedSecondaryItem = null; return; } @@ -2996,9 +3105,13 @@ namespace Barotrauma DoInteractionUpdate(deltaTime, mouseSimPos); } - if (SelectedConstruction != null && !CanInteractWith(SelectedConstruction)) + if (SelectedItem != null && !CanInteractWith(SelectedItem)) { - SelectedConstruction = null; + SelectedItem = null; + } + if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem)) + { + SelectedSecondaryItem = null; } if (!IsDead) { LockHands = false; } @@ -3553,7 +3666,7 @@ namespace Barotrauma string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled); if (!string.IsNullOrEmpty(modifiedMessage)) { - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(info.Name, modifiedMessage, message.MessageType.Value, this); + GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this); } } #endif @@ -3936,7 +4049,8 @@ namespace Barotrauma CharacterHealth.Stun = newStun; if (newStun > 0.0f) { - SelectedConstruction = null; + SelectedItem = SelectedSecondaryItem = null; + if (SelectedCharacter != null) { DeselectCharacter(); } } HealthUpdateInterval = 0.0f; } @@ -3952,77 +4066,79 @@ namespace Barotrauma CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime); } } - if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } - foreach (StatusEffect statusEffect in statusEffectList) + if (statusEffects.TryGetValue(actionType, out var statusEffectList)) { - if (statusEffect.type == ActionType.OnDamaged) + foreach (StatusEffect statusEffect in statusEffectList) { - if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.type == ActionType.OnDamaged) { - if (LastAttacker == null || !LastAttacker.IsPlayer) + if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } + if (statusEffect.OnlyPlayerTriggered) { - continue; + if (LastAttacker == null || !LastAttacker.IsPlayer) + { + continue; + } } } - } - if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || - statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); - statusEffect.Apply(actionType, deltaTime, this, targets); - } - else if (statusEffect.targetLimbs != null) - { - foreach (var limbType in statusEffect.targetLimbs) + if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + targets.Clear(); + targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.Apply(actionType, deltaTime, this, targets); + } + else if (statusEffect.targetLimbs != null) + { + foreach (var limbType in statusEffect.targetLimbs) { - // Target all matching limbs - foreach (var limb in AnimController.Limbs) + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - if (limb.IsSevered) { continue; } - if (limb.type == limbType) + // Target all matching limbs + foreach (var limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type == limbType) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = AnimController.GetLimb(limbType); + if (limb != null) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + if (limb != null) { statusEffect.sourceBody = limb.body; statusEffect.Apply(actionType, deltaTime, this, limb); } } } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) - { - // Target just the first matching limb - Limb limb = AnimController.GetLimb(limbType); - if (limb != null) - { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); - } - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) - { - // Target just the last matching limb - Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); - if (limb != null) - { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); - } - } + } + if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, this, this); } } - if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { - statusEffect.Apply(actionType, deltaTime, this, this); - } - } - if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) - { - // OnDamaged is called only for the limb that is hit. - foreach (Limb limb in AnimController.Limbs) - { - limb.ApplyStatusEffects(actionType, deltaTime); + // OnDamaged is called only for the limb that is hit. + foreach (Limb limb in AnimController.Limbs) + { + limb.ApplyStatusEffects(actionType, deltaTime); + } } } //OnActive effects are handled by the afflictions themselves @@ -4097,10 +4213,12 @@ namespace Barotrauma return; } +#if SERVER if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData(forceAfflictionData: true)); } +#endif isDead = true; @@ -4175,7 +4293,7 @@ namespace Barotrauma } } - SelectedConstruction = null; + SelectedItem = SelectedSecondaryItem = null; SelectedCharacter = null; AnimController.ResetPullJoints(); @@ -4689,7 +4807,7 @@ namespace Barotrauma { foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) { - if (talentOption.Talents.None(t => HasTalent(t.Identifier))) + if (talentOption.TalentIdentifiers.None(t => HasTalent(t))) { return false; } @@ -4918,6 +5036,12 @@ namespace Barotrauma /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs /// public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + + public void StopClimbing() + { + AnimController.StopClimbing(); + SelectedSecondaryItem = null; + } } class ActiveTeamChange diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 731461475..9786888d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Barotrauma.Items.Components; -using Barotrauma.Networking; +using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -25,9 +26,10 @@ namespace Barotrauma UpdateSkills = 12, UpdateMoney = 13, UpdatePermanentStats = 14, + RemoveFromCrew = 15, MinValue = 0, - MaxValue = 14 + MaxValue = 15 } private interface IEventData : NetEntityEvent.IData @@ -54,6 +56,15 @@ namespace Barotrauma public struct CharacterStatusEventData : IEventData { public EventType EventType => EventType.Status; + +#if SERVER + public bool ForceAfflictionData; + + public CharacterStatusEventData(bool forceAfflictionData) + { + ForceAfflictionData = forceAfflictionData; + } +#endif } public struct TreatmentEventData : IEventData @@ -124,18 +135,30 @@ namespace Barotrauma public EventType EventType => EventType.TeamChange; } + [NetworkSerialize] + public readonly record struct ItemTeamChange(CharacterTeamType TeamId, ImmutableArray ItemIds) : INetSerializableStruct; + + public struct AddToCrewEventData : IEventData { public EventType EventType => EventType.AddToCrew; - public readonly CharacterTeamType TeamType; - public readonly ImmutableArray InventoryItems; + public readonly ItemTeamChange ItemTeamChange; public AddToCrewEventData(CharacterTeamType teamType, IEnumerable inventoryItems) { - TeamType = teamType; - InventoryItems = inventoryItems.ToImmutableArray(); + ItemTeamChange = new ItemTeamChange(teamType, inventoryItems.Select(it => it.ID).ToImmutableArray()); + } + } + + public struct RemoveFromCrewEventData : IEventData + { + public EventType EventType => EventType.RemoveFromCrew; + public readonly ItemTeamChange ItemTeamChange; + + public RemoveFromCrewEventData(CharacterTeamType teamType, IEnumerable inventoryItems) + { + ItemTeamChange = new ItemTeamChange(teamType, inventoryItems.Select(it => it.ID).ToImmutableArray()); } - } public struct UpdateExperienceEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 1a9df1f90..b524580df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -261,6 +261,10 @@ namespace Barotrauma public string Name; + public LocalizedString Title; + + public (Identifier NpcSetIdentifier, Identifier NpcIdentifier) HumanPrefabIds; + public string DisplayName { get @@ -649,15 +653,12 @@ namespace Barotrauma { Name = name; } - else if (!npcIdentifier.IsEmpty && TextManager.Get("npctitle." + npcIdentifier) is { Loaded: true } npcTitle) - { - Name = npcTitle.Value; - } else { Name = GetRandomName(randSync); } - + + TryLoadNameAndTitle(npcIdentifier); SetPersonalityTrait(); Salary = CalculateSalary(); @@ -727,7 +728,7 @@ namespace Barotrauma } // Used for loading the data - public CharacterInfo(XElement infoElement) + public CharacterInfo(XElement infoElement, Identifier npcIdentifier = default) { ID = idCounter; idCounter++; @@ -774,6 +775,8 @@ namespace Barotrauma Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); CheckColors(); + TryLoadNameAndTitle(npcIdentifier); + if (string.IsNullOrEmpty(Name)) { var nameElement = CharacterConfigElement.GetChildElement("names"); @@ -794,9 +797,21 @@ namespace Barotrauma ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); if (personalityName != Identifier.Empty) { - PersonalityTrait = NPCPersonalityTrait.Get(GameSettings.CurrentConfig.Language, personalityName); + if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) || + NPCPersonalityTrait.Traits.TryGet(personalityName.Replace(" ".ToIdentifier(), Identifier.Empty), out trait)) + { + PersonalityTrait = trait; + } + else + { + DebugConsole.ThrowError($"Error in CharacterInfo \"{OriginalName}\": could not find a personality trait with the identifier \"{personalityName}\"."); + } } + HumanPrefabIds = ( + infoElement.GetAttributeIdentifier("npcsetid", Identifier.Empty), + infoElement.GetAttributeIdentifier("npcid", Identifier.Empty)); + MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); foreach (var subElement in infoElement.Elements()) @@ -838,6 +853,19 @@ namespace Barotrauma LoadHeadAttachments(); } + private void TryLoadNameAndTitle(Identifier npcIdentifier) + { + if (!npcIdentifier.IsEmpty) + { + Title = TextManager.Get("npctitle." + npcIdentifier); + string nameTag = "charactername." + npcIdentifier; + if (TextManager.ContainsTag(nameTag)) + { + Name = TextManager.Get(nameTag).Value; + } + } + } + private List hairs; public IReadOnlyList Hairs => hairs; private List beards; @@ -1259,6 +1287,11 @@ namespace Barotrauma if (splitTag[0] != "name") { continue; } if (splitTag[1] != Name) { continue; } item.ReplaceTag(tag, $"name:{newName}"); + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.OwnerName = newName; + } break; } } @@ -1292,9 +1325,16 @@ namespace Barotrauma new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), - new XAttribute("personality", PersonalityTrait?.Name.Value ?? "")); + new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty)); // TODO: animations? + if (HumanPrefabIds != default) + { + charElement.Add( + new XAttribute("npcsetid", HumanPrefabIds.NpcSetIdentifier), + new XAttribute("npcid", HumanPrefabIds.NpcIdentifier)); + } + charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); if (Character != null) @@ -1323,11 +1363,8 @@ namespace Barotrauma } } - - charElement.Add(savedStatElement); - - parentElement.Add(charElement); + parentElement?.Add(charElement); return charElement; } @@ -1597,9 +1634,9 @@ namespace Barotrauma return id; } - public static void ApplyHealthData(Character character, XElement healthData) + public static void ApplyHealthData(Character character, XElement healthData, Func afflictionPredicate = null) { - if (healthData != null) { character?.CharacterHealth.Load(healthData); } + if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs index 4f0068328..5d8e294ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs @@ -10,26 +10,27 @@ namespace Barotrauma public readonly Direction Direction; public readonly Character SelectedCharacter; - public readonly Item SelectedItem; + public readonly Item SelectedItem, SelectedSecondaryItem; public readonly AnimController.Animation Animation; - public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None) - : this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, animation) + public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) + : this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation) { } - public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None) - : this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, animation) + public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) + : this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation) { } - protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None) + protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) : base(pos, rotation, velocity, angularVelocity, ID, time) { Direction = dir; SelectedCharacter = selectedCharacter; SelectedItem = selectedItem; + SelectedSecondaryItem = selectedSecondaryItem; Animation = animation; } @@ -81,10 +82,10 @@ namespace Barotrauma public UInt16 networkUpdateID; } - private List memInput = new List(); + private readonly List memInput = new List(); - private List memState = new List(); - private List memLocalState = new List(); + private readonly List memState = new List(); + private readonly List memLocalState = new List(); public float healthUpdateTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index f41b7720f..cb1b33763 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -12,12 +12,8 @@ namespace Barotrauma { public readonly static PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); Character.RemoveByPrefab(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs index 2b9602b33..f34b8ae11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs @@ -11,13 +11,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } public static CorpsePrefab Get(Identifier identifier) { @@ -46,7 +40,7 @@ namespace Barotrauma [Serialize(0, IsPropertySaveable.No)] public int MaxMoney { get; private set; } - public CorpsePrefab(ContentXElement element, CorpsesFile file) : base(element, file) { } + public CorpsePrefab(ContentXElement element, CorpsesFile file) : base(element, file, npcSetIdentifier: Identifier.Empty) { } public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index d1ceb4b38..8d4b7951b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -50,6 +50,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] public float Probability { get; set; } = 1.0f; + [Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable] + public bool DivideByLimbCount { get; set; } + public float DamagePerSecond; public float DamagePerSecondTimer; public float PreviousVitalityDecrease; @@ -96,9 +99,11 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public Affliction CreateMultiplied(float multiplier) + public Affliction CreateMultiplied(float multiplier, float probability) { - return Prefab.Instantiate(NonClampedStrength * multiplier, Source); + var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); + instance.Probability = probability; + return instance; } public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 321892133..a030db96b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -451,13 +451,12 @@ namespace Barotrauma public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab) { - return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName); + return new Identifier(speciesName.Value + prefab.HuskedSpeciesName.Value); } public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab) { - Identifier nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); - return huskedSpeciesName.Remove(nonTag); + return huskedSpeciesName.Remove(prefab.HuskedSpeciesName); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 04d620a18..a18f012bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -63,8 +63,10 @@ namespace Barotrauma if (HuskedSpeciesName.IsEmpty) { DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); - HuskedSpeciesName = "[speciesname]husk".ToIdentifier(); + HuskedSpeciesName = "husk".ToIdentifier(); } + // Remove "[speciesname]" for backward support (we don't use it anymore) + HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier(); TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { @@ -108,7 +110,6 @@ namespace Barotrauma public readonly Identifier HuskedSpeciesName; public readonly Identifier[] TargetSpecies; - public static readonly Identifier Tag = "[speciesname]".ToIdentifier(); public readonly bool TransferBuffs; public readonly bool SendMessages; @@ -404,8 +405,18 @@ namespace Barotrauma AfflictionType = element.GetAttributeIdentifier("type", ""); TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier); - Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}").Fallback(element.GetAttributeString("name", "")); - Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}").Fallback(element.GetAttributeString("description", "")); + Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}"); + string fallbackName = element.GetAttributeString("name", ""); + if (!string.IsNullOrEmpty(fallbackName)) + { + Name = Name.Fallback(fallbackName); + } + Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); + string fallbackDescription = element.GetAttributeString("description", ""); + if (!string.IsNullOrEmpty(fallbackDescription)) + { + Description = Description.Fallback(fallbackDescription); + } IsBuff = element.GetAttributeBool("isbuff", false); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 642d39748..8a569ba1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -60,13 +60,27 @@ namespace Barotrauma if (vitalityMultipliers != null) { float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); + foreach (var vitalityMultiplier in vitalityMultipliers) + { + VitalityMultipliers.Add(vitalityMultiplier, multiplier); + if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier)) + { + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?"); + } + } } var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); if (vitalityTypeMultipliers != null) { float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - vitalityTypeMultipliers.ForEach(i => VitalityTypeMultipliers.Add(i, multiplier)); + foreach (var vitalityTypeMultiplier in vitalityTypeMultipliers) + { + VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier); + if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier)) + { + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?"); + } + } } if (vitalityMultipliers == null && VitalityTypeMultipliers == null) { @@ -911,19 +925,11 @@ namespace Barotrauma float vitalityDecrease = affliction.GetVitalityDecrease(this); if (limbHealth != null) { - if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier)) - { - vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier]; - } - if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType)) - { - vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType]; - } + vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth); } Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } - #if CLIENT if (IsUnconscious) { @@ -932,6 +938,33 @@ namespace Barotrauma #endif } + private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth) + { + float multiplier = 1.0f; + if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier)) + { + multiplier *= vitalityMultiplier; + } + if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier)) + { + multiplier *= vitalityTypeMultiplier; + } + return multiplier; + } + + /// + /// How much vitality the affliction reduces, taking into account the effects of vitality modifiers on the limb the affliction is on (if limb-based) + /// + private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction) + { + float vitalityDecrease = affliction.GetVitalityDecrease(this); + if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null) + { + vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth); + } + return vitalityDecrease; + } + private void Kill() { if (Unkillable || Character.GodMode) { return; } @@ -1021,7 +1054,7 @@ namespace Barotrauma /// A dictionary where the key is the identifier of the item and the value the suitability /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -1045,7 +1078,18 @@ namespace Barotrauma } if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } - if (ignoreHiddenAfflictions && strength < affliction.Prefab.ShowIconThreshold) { continue; } + + if (ignoreHiddenAfflictions) + { + if (user == Character) + { + if (strength < affliction.Prefab.ShowIconThreshold) { continue; } + } + else + { + if (strength < affliction.Prefab.ShowIconToOthersThreshold) { continue; } + } + } foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { @@ -1129,17 +1173,17 @@ namespace Barotrauma activeAfflictions.Add(affliction); } } - msg.Write((byte)activeAfflictions.Count); + msg.WriteByte((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { - msg.Write(affliction.Prefab.UintIdentifier); + msg.WriteUInt32(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); - msg.Write((byte)affliction.Prefab.PeriodicEffects.Count()); + msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count); foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects) { - msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); + msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], 0, periodicEffect.MaxInterval, 8); } } @@ -1153,15 +1197,15 @@ namespace Barotrauma limbAfflictions.Add((limbHealth, limbAffliction)); } - msg.Write((byte)limbAfflictions.Count); + msg.WriteByte((byte)limbAfflictions.Count); foreach (var (limbHealth, affliction) in limbAfflictions) { msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1); - msg.Write(affliction.Prefab.UintIdentifier); + msg.WriteUInt32(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); - msg.Write((byte)affliction.Prefab.PeriodicEffects.Count()); + msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count); foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects) { msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); @@ -1210,7 +1254,7 @@ namespace Barotrauma } } - public void Load(XElement element) + public void Load(XElement element, Func afflictionPredicate = null) { foreach (var subElement in element.Elements()) { @@ -1243,6 +1287,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found."); return; } + if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; } float strength = afflictionElement.GetAttributeFloat("strength", 0.0f); var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab); if (irremovableAffliction != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 07f16e007..a71d1e73e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -86,6 +86,28 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); } + foreach (var afflictionType in parsedAfflictionTypes) + { + if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); + } + } + foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) + { + if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + } + } + static void createWarningOrError(string msg) + { +#if DEBUG + DebugConsole.ThrowError(msg); +#else + DebugConsole.AddWarning(msg); +#endif + } } private void ParseAfflictionTypes() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 7e8a539ff..e3d7bedf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -10,7 +10,7 @@ namespace Barotrauma class HumanPrefab : PrefabWithUintIdentifier { [Serialize("any", IsPropertySaveable.No)] - public string Job { get; protected set; } + public Identifier Job { get; protected set; } [Serialize(1f, IsPropertySaveable.No)] public float Commonness { get; protected set; } @@ -82,17 +82,19 @@ namespace Barotrauma public XElement Element { get; protected set; } - public readonly Dictionary ItemSets = new Dictionary(); - public readonly Dictionary CustomNPCSets = new Dictionary(); + public readonly List<(XElement element, float commonness)> ItemSets = new List<(XElement element, float commonness)>(); + public readonly List<(XElement element, float commonness)> CustomCharacterInfos = new List<(XElement element, float commonness)>(); - public HumanPrefab(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) + public readonly Identifier NpcSetIdentifier; + + public HumanPrefab(ContentXElement element, ContentFile file, Identifier npcSetIdentifier) : base(file, element.GetAttributeIdentifier("identifier", "")) { SerializableProperty.DeserializeProperties(this, element); - Job = Job.ToLowerInvariant(); Element = element; - element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); - element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1))); + element.GetChildElements("itemset").ForEach(e => ItemSets.Add((e, e.GetAttributeFloat("commonness", 1)))); + element.GetChildElements("character").ForEach(e => CustomCharacterInfos.Add((e, e.GetAttributeFloat("commonness", 1)))); PreferredOutpostModuleTypes = element.GetAttributeIdentifierArray("preferredoutpostmoduletypes", Array.Empty()); + this.NpcSetIdentifier = npcSetIdentifier; } public IEnumerable GetModuleFlags() @@ -107,7 +109,7 @@ namespace Barotrauma public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced, Func predicate = null) { - return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate); + return !Job.IsEmpty && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate); } public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) @@ -146,23 +148,43 @@ namespace Barotrauma } } - public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) + public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { - if (ItemSets == null || !ItemSets.Any()) { return; } - var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync); + if (ItemSets == null || !ItemSets.Any()) { return false; } + var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; if (spawnItems != null) { foreach (XElement itemElement in spawnItems.GetChildElements("item")) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + int amount = itemElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) + { + InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + } } } + return true; } - public CharacterInfo GetCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced) + /// + /// Creates a character info from the human prefab. If there are custom character infos defined, those are used, otherwise a randomized info is generated. + /// + /// + /// + public CharacterInfo CreateCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced) { - var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), randSync); - return characterElement != null ? new CharacterInfo(characterElement) : null; + var characterElement = ToolBox.SelectWeightedRandom(CustomCharacterInfos, info => info.commonness, randSync).element; + CharacterInfo characterInfo; + if (characterElement == null) + { + characterInfo= new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier); + } + else + { + characterInfo = new CharacterInfo(characterElement, Identifier); + } + characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier); + return characterInfo; } public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) @@ -229,7 +251,11 @@ namespace Barotrauma parentItem?.Combine(item, user: null); foreach (XElement childItemElement in itemElement.Elements()) { - InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + int amount = childItemElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) + { + InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 615a33350..e1748b990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -19,7 +19,7 @@ namespace Barotrauma public int Variant; - public Skill PrimarySkill { get; } + public Skill PrimarySkill { get; private set; } public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { } @@ -102,9 +102,14 @@ namespace Barotrauma public void OverrideSkills(Dictionary newSkills) { skills.Clear(); - foreach (var newSkill in newSkills) + foreach (var newSkillInfo in newSkills) { - skills.Add(newSkill.Key, new Skill(newSkill.Key, newSkill.Value)); + var newSkill = new Skill(newSkillInfo.Key, newSkillInfo.Value); + if (PrimarySkill != null && newSkill.Identifier == PrimarySkill.Identifier) + { + PrimarySkill = newSkill; + } + skills.Add(newSkillInfo.Key, newSkill); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 8153bca07..2db2aaadf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -65,13 +65,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } private static readonly Dictionary _itemRepairPriorities = new Dictionary(); /// @@ -79,7 +73,7 @@ namespace Barotrauma /// public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; - public static JobPrefab Get(string identifier) + public static JobPrefab Get(Identifier identifier) { if (Prefabs.ContainsKey(identifier)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index e958f391f..030e232b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -218,6 +218,8 @@ namespace Barotrauma public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; + public Hull Hull; + public bool InWater { get; set; } private FixedMouseJoint pullJoint; @@ -303,6 +305,17 @@ namespace Barotrauma public Vector2 DebugTargetPos; public Vector2 DebugRefPos; + public bool IsLowerBody => + type == LimbType.LeftLeg || + type == LimbType.RightLeg || + type == LimbType.LeftFoot || + type == LimbType.RightFoot || + type == LimbType.Tail || + type == LimbType.Legs || + type == LimbType.RightThigh || + type == LimbType.LeftThigh || + type == LimbType.Waist; + public bool IsSevered { get { return isSevered; } @@ -709,11 +722,12 @@ namespace Barotrauma tempModifiers.Clear(); var newAffliction = affliction; float random = Rand.Value(Rand.RandSync.Unsynced); - if (random > affliction.Probability) { continue; } + bool foundMatchingModifier = false; bool applyAffliction = true; foreach (DamageModifier damageModifier in DamageModifiers) { if (!damageModifier.MatchesAffliction(affliction)) { continue; } + foundMatchingModifier = true; if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) { applyAffliction = false; @@ -729,6 +743,7 @@ namespace Barotrauma foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { if (!damageModifier.MatchesAffliction(affliction)) { continue; } + foundMatchingModifier = true; if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) { applyAffliction = false; @@ -740,6 +755,7 @@ namespace Barotrauma } } } + if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; foreach (DamageModifier damageModifier in tempModifiers) { @@ -752,7 +768,7 @@ namespace Barotrauma } if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) { - newAffliction = affliction.CreateMultiplied(finalDamageModifier); + newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs index 292b58fc6..cc14d0fc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs @@ -5,9 +5,11 @@ using System.Xml.Linq; namespace Barotrauma { - class NPCPersonalityTrait + class NPCPersonalityTrait : PrefabWithUintIdentifier { - public readonly Identifier Name; + public readonly static PrefabCollection Traits = new PrefabCollection(); + + public readonly LocalizedString DisplayName; public readonly List AllowedDialogTags; @@ -17,43 +19,29 @@ namespace Barotrauma get { return commonness; } } - public static IEnumerable GetAll(LanguageIdentifier language) + public NPCPersonalityTrait(XElement element, NPCPersonalityTraitsFile file) + : base(file, element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("name", Identifier.Empty))) { - if (language != TextManager.DefaultLanguage && !NPCConversationCollection.Collections.ContainsKey(language)) + string name = element.GetAttributeString("name", null); + if (name == null) { - DebugConsole.AddWarning($"Could not find NPC personality traits for the language \"{language}\". Using \"{TextManager.DefaultLanguage}\" instead.."); - language = TextManager.DefaultLanguage; + DisplayName = TextManager.Get("personalitytrait." + Identifier) + .Fallback(Identifier.ToString()); } - return NPCConversationCollection.Collections[language] - .SelectMany(cc => cc.PersonalityTraits.Values); - } - - public static NPCPersonalityTrait Get(LanguageIdentifier language, Identifier traitName) - { - if (language != TextManager.DefaultLanguage && !NPCConversationCollection.Collections.ContainsKey(language)) + else { - DebugConsole.AddWarning($"Could not find NPC personality traits for the language \"{language}\". Using \"{TextManager.DefaultLanguage}\" instead.."); - language = TextManager.DefaultLanguage; + DisplayName = name; } - return NPCConversationCollection.Collections[language] - .FirstOrDefault(cc => cc.PersonalityTraits.ContainsKey(traitName)) - .PersonalityTraits[traitName]; - } - - public NPCPersonalityTrait(XElement element) - { - Name = element.GetAttributeIdentifier("name", ""); AllowedDialogTags = new List(element.GetAttributeStringArray("alloweddialogtags", Array.Empty())); commonness = element.GetAttributeFloat("commonness", 1.0f); } public static NPCPersonalityTrait GetRandom(string seed) { - #warning TODO: implement NPCPersonality content type and revise this for determinism var rand = new MTRandom(ToolBox.StringToInt(seed)); - var list = GetAll(GameSettings.CurrentConfig.Language); - return ToolBox.SelectWeightedRandom(list, t => t.commonness, rand); + return ToolBox.SelectWeightedRandom(Traits.OrderBy(t => t.UintIdentifier), t => t.commonness, rand); } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs index de5597011..e3997e128 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionAboveVitality : AbilityConditionDataless { @@ -13,7 +11,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - return character.HealthPercentage / 100f > vitalityPercentage; + return character.Vitality / character.MaxVitality > vitalityPercentage; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs index 55a640a17..92f7af301 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs @@ -1,11 +1,10 @@ using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionAlliesAboveVitality : AbilityConditionDataless { - float vitalityPercentage; + readonly float vitalityPercentage; public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs index 50c650f1d..b368fad6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionCoauthor : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs index cc44ec6ac..e15b36dec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionCrouched : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs index 68c46cd12..8470362c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -1,28 +1,22 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionHasAffliction : AbilityConditionDataless { - private string afflictionIdentifier; - private float minimumPercentage; - + private readonly Identifier afflictionIdentifier; + private readonly float minimumPercentage; public AbilityConditionHasAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", ""); + afflictionIdentifier = conditionElement.GetAttributeIdentifier("afflictionidentifier", Identifier.Empty); minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f); } protected override bool MatchesConditionSpecific() { - if (!string.IsNullOrEmpty(afflictionIdentifier)) + if (!afflictionIdentifier.IsEmpty) { var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier); - if (affliction == null) { return false; } - return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength; } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 4407fcb18..04d7ebf62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -3,55 +3,43 @@ using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Abilities { class AbilityConditionHasItem : AbilityConditionDataless { - // not used for anything atm, will be used for clown subclass private readonly string[] tags; - private InvSlotType? invSlotType; - bool requireAll; - - private List items = new List(); + readonly bool requireAll; public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty()); requireAll = conditionElement.GetAttributeBool("requireall", false); - //this.invSlotType = invSlotType; } protected override bool MatchesConditionSpecific() { - items.Clear(); - if (tags.Any()) + if (tags.None()) { - foreach (string tag in tags) - { - // there is a better method, should use that instead - if (character.GetEquippedItem(tag, invSlotType) is Item foundItem) - { - items.Add(foundItem); - } - } - - } - else - { - if (character.GetEquippedItem(null, invSlotType) is Item foundItem) - { - items.Add(foundItem); - } + return character.GetEquippedItem(null) is Item; } if (requireAll) { - return (items.Count >= tags.Count()); + foreach (string tag in tags) + { + if (character.GetEquippedItem(tag) == null) { return false; } + } + return true; } else { - return items.Any(); + foreach (string tag in tags) + { + if (character.GetEquippedItem(tag) != null) { return true; } + } + return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs index d54fd0839..b6abddd3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionHasVelocity : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs index f75f98a89..a8d33433a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionShipFlooded : AbilityConditionDataless { @@ -14,8 +11,14 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { if (!character.IsInFriendlySub) { return false; } - float currentFloodPercentage = character.Submarine.GetHulls(false).Average(h => h.WaterPercentage); - return currentFloodPercentage / 100 > floodPercentage; + float waterVolume = 0.0f, totalVolume = 0.0f; + foreach (Hull hull in Hull.HullList) + { + if (hull.Submarine != character.Submarine) { continue; } + waterVolume += hull.WaterVolume; + totalVolume += hull.Volume; + } + return (waterVolume / totalVolume) > floodPercentage; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs index 29d1fdf3c..d17d62b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyStatToFlooding : CharacterAbility { @@ -22,8 +19,14 @@ namespace Barotrauma.Abilities if (conditionsMatched && Character.IsInFriendlySub) { - float currentFloodPercentage = Character.Submarine.GetHulls(false).Average(h => h.WaterPercentage); - lastValue = currentFloodPercentage / 100f * maxValue; + float waterVolume = 0.0f, totalVolume = 0.0f; + foreach (Hull hull in Hull.HullList) + { + if (hull.Submarine != Character.Submarine) { continue; } + waterVolume += hull.WaterVolume; + totalVolume += hull.Volume; + } + lastValue = (totalVolume == 0.0f ? 1.0f : waterVolume / totalVolume) * maxValue; Character.ChangeStat(statType, lastValue); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs index c76b7fb01..e8d0ad788 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityUnlockTree : CharacterAbility { @@ -14,22 +10,19 @@ namespace Barotrauma.Abilities { if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier)); if (subTree == null) { return; } subTree.ForceUnlock = true; if (!addingFirstTime) { return; } - foreach (var talentOption in subTree.TalentOptionStages) + foreach (var talentId in subTree.AllTalentIdentifiers) { - foreach (var talent in talentOption.Talents) + if (talentId == CharacterTalent.Prefab.Identifier) { continue; } + if (Character.GiveTalent(talentId)) { - if (talent == CharacterTalent.Prefab) { continue; } - if (Character.GiveTalent(talent)) - { - Character.Info.AdditionalTalentPoints++; - } - } + Character.Info.AdditionalTalentPoints++; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index 3ea78fd56..982cb2371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -1,10 +1,4 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityInsurancePolicy : CharacterAbility { @@ -19,10 +13,10 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { - if (Character?.Info is CharacterInfo info) + if (Character?.Info is CharacterInfo info && GameMain.GameSession?.GameMode is CampaignMode campaign) { int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath; - Character.GiveMoney(totalAmount); + campaign.Bank.Give(totalAmount); GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 5bcac5e83..d58d73b49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -1,8 +1,4 @@ -using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Abilities { @@ -17,7 +13,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } + if (!SelectedItemHasTag(Character)) { return; } Character closestCharacter = null; float closestDistance = squaredMaxDistance; @@ -31,13 +27,17 @@ namespace Barotrauma.Abilities } } - if (closestCharacter?.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; } + if (closestCharacter == null || !SelectedItemHasTag(closestCharacter)) { return; } if (closestDistance < squaredMaxDistance) { ApplyEffectSpecific(Character); ApplyEffectSpecific(closestCharacter); } + + bool SelectedItemHasTag(Character character) => + (character.SelectedItem != null && character.SelectedItem.HasTag(tag)) || + (character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 656e5d751..bd4b45998 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities public readonly AbilityEffectType AbilityEffectType; - protected int maxTriggerCount { get; } + protected readonly int maxTriggerCount; protected int timesTriggered = 0; @@ -88,8 +88,6 @@ namespace Barotrauma.Abilities // XML private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { - AbilityCondition newCondition = null; - Type conditionType; string type = conditionElement.Name.ToString().ToLowerInvariant(); try @@ -109,6 +107,7 @@ namespace Barotrauma.Abilities object[] args = { characterTalent, conditionElement }; + AbilityCondition newCondition; try { newCondition = (AbilityCondition)Activator.CreateInstance(conditionType, args); @@ -210,8 +209,7 @@ namespace Barotrauma.Abilities public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier) { - AbilityFlags flagType = AbilityFlags.None; - if (!Enum.TryParse(flagTypeString, true, out flagType)) + if (!Enum.TryParse(flagTypeString, true, out AbilityFlags flagType)) { DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 2e505890e..d8f954873 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -56,11 +56,6 @@ namespace Barotrauma } } - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 0d6c08118..e3379314f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -19,7 +18,12 @@ namespace Barotrauma public static readonly PrefabCollection JobTalentTrees = new PrefabCollection(); - public readonly List TalentSubTrees = new List(); + public readonly ImmutableArray TalentSubTrees; + + /// + /// Talent identifiers of all the talents in this tree + /// + public readonly ImmutableHashSet AllTalentIdentifiers; public ContentXElement ConfigElement { @@ -36,16 +40,19 @@ namespace Barotrauma DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - + + List subTrees = new List(); foreach (var subTreeElement in element.GetChildElements("subtree")) { - TalentSubTrees.Add(new TalentSubTree(subTreeElement)); + subTrees.Add(new TalentSubTree(subTreeElement)); } + TalentSubTrees = subTrees.ToImmutableArray(); + AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet(); } public bool TalentIsInTree(Identifier talentIdentifier) { - return TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))).Any(c => c == talentIdentifier); + return AllTalentIdentifiers.Contains(talentIdentifier); } public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) @@ -54,6 +61,7 @@ namespace Barotrauma } // i hate this function - markus + // me too - joonas public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) { if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } @@ -66,12 +74,12 @@ namespace Barotrauma TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.Talents.Any(t => character.HasTalent(t.Identifier))) + if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t))) { return TalentTreeStageState.Unlocked; } - if (targetTalentOption.Talents.Any(t => selectedTalents.Contains(t.Identifier))) + if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t))) { return TalentTreeStageState.Highlighted; } @@ -83,8 +91,8 @@ namespace Barotrauma if (lastindex >= 0) { TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex]; - hasTalentInLastTier = lastLatentOption.Talents.Any(HasTalent); - isLastTalentPurchased = lastLatentOption.Talents.Any(t => character.HasTalent(t.Identifier)); + hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent); + isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t)); } if (!hasTalentInLastTier) @@ -101,9 +109,9 @@ namespace Barotrauma return TalentTreeStageState.Locked; - bool HasTalent(TalentPrefab t) + bool HasTalent(Identifier talentId) { - return selectedTalents.Contains(t.Identifier); + return selectedTalents.Contains(talentId); } } @@ -117,14 +125,14 @@ namespace Barotrauma foreach (var subTree in talentTree.TalentSubTrees) { - if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.Talents.Any(t => t.Identifier == talentIdentifier))) { return true; } + if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; } foreach (var talentOptionStage in subTree.TalentOptionStages) { - bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier)); + bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t)); if (!hasTalentInThisTier) { - if (talentOptionStage.Talents.Any(t => t.Identifier == talentIdentifier)) + if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) { return true; } @@ -170,18 +178,21 @@ namespace Barotrauma public bool ForceUnlock; - public readonly List TalentOptionStages = new List(); + public readonly ImmutableArray TalentOptionStages; + + public readonly ImmutableHashSet AllTalentIdentifiers; public TalentSubTree(ContentXElement subTreeElement) { Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); - + List talentOptionStages = new List(); foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { - TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); + talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } + TalentOptionStages = talentOptionStages.ToImmutableArray(); + AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet(); } } @@ -190,8 +201,12 @@ namespace Barotrauma { private readonly ImmutableHashSet talentIdentifiers; - public IEnumerable Talents - => talentIdentifiers.Select(id => TalentPrefab.TalentPrefabs[id]); + public IEnumerable TalentIdentifiers => talentIdentifiers; + + public bool HasTalent(Identifier talentIdentifier) + { + return talentIdentifiers.Contains(talentIdentifier); + } public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index cd0906887..b57133514 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -40,7 +40,9 @@ namespace Barotrauma } catch { - prefab.Dispose(); //clean up before rethrowing, since some prefab types might lock resources + //clean up before rethrowing, since some prefab types might lock resources + prefab.Dispose(); + Prefabs.Remove(prefab); throw; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCPersonalityTraitsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCPersonalityTraitsFile.cs new file mode 100644 index 000000000..98ea05e9f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCPersonalityTraitsFile.cs @@ -0,0 +1,12 @@ +namespace Barotrauma +{ + sealed class NPCPersonalityTraitsFile : GenericPrefabFile + { + public NPCPersonalityTraitsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "personalitytrait"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "personalitytraits"; + protected override PrefabCollection Prefabs => NPCPersonalityTrait.Traits; + protected override NPCPersonalityTrait CreatePrefab(ContentXElement element) => new NPCPersonalityTrait(element, this); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs index 57b5eaf64..6357712a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs @@ -1,5 +1,3 @@ -using Barotrauma; - namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TutorialsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TutorialsFile.cs new file mode 100644 index 000000000..a2409d954 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TutorialsFile.cs @@ -0,0 +1,16 @@ +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TutorialsFile : GenericPrefabFile + { + protected override PrefabCollection Prefabs => TutorialPrefab.Prefabs; + + public TutorialsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "Tutorial"; + + protected override bool MatchesPlural(Identifier identifier) => identifier == "Tutorials"; + + protected override TutorialPrefab CreatePrefab(ContentXElement element) => new TutorialPrefab(this, element); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 1cfef53ce..d99d29b57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -29,25 +29,25 @@ namespace Barotrauma public readonly ImmutableArray AltNames; public readonly string Path; public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; - public readonly UInt64 SteamWorkshopId; + public readonly Option UgcId; public readonly Version GameVersion; public readonly string ModVersion; public Md5Hash Hash { get; private set; } - public readonly DateTime? InstallTime; + public readonly Option InstallTime; public ImmutableArray Files { get; private set; } public ImmutableArray Errors { get; private set; } public async Task IsUpToDate() { - if (SteamWorkshopId != 0 && InstallTime.HasValue) - { - Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId); - if (item is null) { return true; } - return item.Value.LatestUpdateTime <= InstallTime; - } - return true; + if (!UgcId.TryUnwrap(out var ugcId)) { return true; } + if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; } + if (!InstallTime.TryUnwrap(out var installTime)) { return true; } + + Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); + if (item is null) { return true; } + return item.Value.LatestUpdateTime <= installTime; } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); @@ -66,18 +66,19 @@ namespace Barotrauma AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) .Select(n => n.Trim()).ToImmutableArray(); AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); - SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + UgcId = steamWorkshopId != 0 + ? Option.Some(new SteamWorkshopId(steamWorkshopId)) + : Option.None(); GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); - if (rootElement.Attribute("installtime") != null) - { - InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0)); - } - else - { - InstallTime = null; - } + UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); + InstallTime = installTimeUnix != 0 + ? Option.Some(ToolBox.Epoch.ToDateTime(installTimeUnix)) + : Option.None(); var fileResults = rootElement.Elements() .Select(e => ContentFile.CreateFromXElement(this, e)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs new file mode 100644 index 000000000..6a9f22ba3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Barotrauma +{ + public abstract class ContentPackageId + { + public abstract string StringRepresentation { get; } + + public override string ToString() + => StringRepresentation; + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public static Option Parse(string s) + => ReflectionUtils.ParseDerived(s); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs new file mode 100644 index 000000000..6e8411d9d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; +using System.Globalization; + +namespace Barotrauma +{ + sealed class SteamWorkshopId : ContentPackageId + { + public readonly UInt64 Value; + + public SteamWorkshopId(UInt64 value) + { + Value = value; + } + + private const string Prefix = "STEAM_WORKSHOP_"; + + public override string StringRepresentation => Value.ToString(CultureInfo.InvariantCulture); + + public override bool Equals(object? obj) + => obj is SteamWorkshopId otherWorkshopId && otherWorkshopId.Value == Value; + + public override int GetHashCode() => Value.GetHashCode(); + + public new static Option Parse(string s) + { + if (s.StartsWith(Prefix)) { s = s[Prefix.Length..]; } + if (!UInt64.TryParse(s, out var id) || id == 0) { return Option.None(); } + return Option.Some(new SteamWorkshopId(id)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index f987aca9d..3cb708a6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Steam; @@ -182,7 +181,7 @@ namespace Barotrauma { if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) { - SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ?? + SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ?? ContentPackageManager.CorePackages.First()); } @@ -194,7 +193,7 @@ namespace Barotrauma newRegular.Add(p); } else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 - => p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP) + => p2.UgcId == p.UgcId) is { } newP) { newRegular.Add(newP); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index b7388bb2b..4e0a4328b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -43,10 +43,10 @@ namespace Barotrauma cachedValue = cachedValue .Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase) .Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase); - if (ContentPackage.SteamWorkshopId != 0) + if (ContentPackage.UgcId.TryUnwrap(out var ugcId)) { cachedValue = cachedValue - .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); + .Replace(string.Format(OtherModDirFmt, ugcId.StringRepresentation), modPath, StringComparison.OrdinalIgnoreCase); } } var allPackages = ContentPackageManager.AllPackages; @@ -55,9 +55,9 @@ namespace Barotrauma #endif foreach (Identifier otherModName in otherMods) { - if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } + Option ugcId = ContentPackageId.Parse(otherModName.Value); ContentPackage? otherMod = - allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId) + allPackages.FirstOrDefault(p => ugcId == p.UgcId) ?? allPackages.FirstOrDefault(p => p.Name == otherModName) ?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName)) ?? throw new MissingContentPackageException(ContentPackage, otherModName.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index bdde120e8..e381b89ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -84,7 +84,9 @@ namespace Barotrauma { static readonly List Coroutines = new List(); - public static float UnscaledDeltaTime, DeltaTime; + public static float DeltaTime { get; private set; } + + public static bool Paused { get; private set; } public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "", bool useSeparateThread = false) { @@ -191,7 +193,7 @@ namespace Barotrauma if (current != null) { if (current.EndsCoroutine(handle) || handle.AbortRequested) { return true; } - if (!current.CheckFinished(UnscaledDeltaTime)) { return false; } + if (!current.CheckFinished(DeltaTime)) { return false; } } if (!handle.Coroutine.MoveNext()) { return true; } return false; @@ -204,7 +206,7 @@ namespace Barotrauma while (!handle.AbortRequested) { if (PerformCoroutineStep(handle)) { return; } - Thread.Sleep((int)(UnscaledDeltaTime * 1000)); + Thread.Sleep((int)(DeltaTime * 1000)); } } catch (ThreadAbortException) @@ -232,7 +234,7 @@ namespace Barotrauma { if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped)) { - if (handle.Exception!=null || handle.Coroutine.Current == CoroutineStatus.Failure) + if (handle.Exception != null || handle.Coroutine.Current == CoroutineStatus.Failure) { DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); } @@ -254,9 +256,9 @@ namespace Barotrauma #endif } // Updating just means stepping through all the coroutines - public static void Update(float unscaledDeltaTime, float deltaTime) + public static void Update(bool paused, float deltaTime) { - UnscaledDeltaTime = unscaledDeltaTime; + Paused = paused; DeltaTime = deltaTime; List coroutineList; @@ -276,14 +278,27 @@ namespace Barotrauma } } } + + public static void ListCoroutines() + { + lock (Coroutines) + { + DebugConsole.NewMessage("***********"); + DebugConsole.NewMessage($"{Coroutines.Count} coroutine(s)"); + foreach (var c in Coroutines) + { + DebugConsole.NewMessage($"- {c.Name}"); + } + } + } } class WaitForSeconds : CoroutineStatus { public readonly float TotalTime; - float timer; - bool ignorePause; + private float timer; + private readonly bool ignorePause; public WaitForSeconds(float time, bool ignorePause = true) { @@ -295,7 +310,7 @@ namespace Barotrauma public override bool CheckFinished(float deltaTime) { #if !SERVER - if (ignorePause || !GUI.PauseMenuOpen) + if (ignorePause || !CoroutineManager.Paused) { timer -= deltaTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 759144783..077fe7d64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -431,7 +431,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || args.Length == 0) return; int.TryParse(args[0], out int id); - var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id); + var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id); if (client == null) { ThrowError("Client id \"" + id + "\" not found."); @@ -467,7 +467,7 @@ namespace Barotrauma banDuration = parsedBanDuration; } - GameMain.NetworkMember.BanPlayer(clientName, reason, false, banDuration); + GameMain.NetworkMember.BanPlayer(clientName, reason, banDuration); }); }); }, @@ -486,7 +486,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || args.Length == 0) return; int.TryParse(args[0], out int id); - var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id); + var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id); if (client == null) { ThrowError("Client id \"" + id + "\" not found."); @@ -510,12 +510,12 @@ namespace Barotrauma banDuration = parsedBanDuration; } - GameMain.NetworkMember.BanPlayer(client.Name, reason, false, banDuration); + GameMain.NetworkMember.BanPlayer(client.Name, reason, banDuration); }); }); })); - commands.Add(new Command("banendpoint|banip", "banendpoint [endpoint]: Ban the IP address/SteamID from the server.", null)); + commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null)); 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, () => @@ -799,7 +799,55 @@ namespace Barotrauma eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(id => id.Value).ToArray() }; })); - + + commands.Add(new Command("unlockmission", "unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (string[] args) => + { + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + { + ThrowError("The unlockmission command is only usable in the campaign mode."); + return; + } + if (args.Length == 0) + { + ThrowError("Please enter the identifier or a tag of the mission you want to unlock."); + return; + } + var currentLocation = campaign.Map.CurrentLocation; + if (MissionPrefab.Prefabs.Any(p => p.Identifier == args[0])) + { + currentLocation.UnlockMissionByIdentifier(args[0].ToIdentifier()); + } + else + { + currentLocation.UnlockMissionByTag(args[0].ToIdentifier()); + } + if (campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); + } + }, isCheat: true, getValidArgs: () => + { + return new[] + { + MissionPrefab.Prefabs.Select(p => p.Identifier.ToString()).ToArray() + }; + })); + + commands.Add(new Command("setcampaignmetadata", "setcampaignmetadata [identifier] [value]: Sets the specified campaign metadata value.", (string[] args) => + { + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + { + ThrowError("The setcampaignmetadata command is only usable in the campaign mode."); + return; + } + if (args.Length < 2) + { + ThrowError("Please specify an identifier and a value."); + return; + } + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + }, isCheat: true)); + commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) => { if (args.Length < 2) @@ -918,16 +966,10 @@ namespace Barotrauma foreach (var talentTree in talentTrees) { - foreach (var subTree in talentTree.TalentSubTrees) + foreach (var talentId in talentTree.AllTalentIdentifiers) { - foreach (var option in subTree.TalentOptionStages) - { - foreach (var talent in option.Talents) - { - character.GiveTalent(talent); - NewMessage($"Unlocked talent \"{talent.DisplayName}\"."); - } - } + character.GiveTalent(talentId); + NewMessage($"Unlocked talent \"{talentId}\"."); } } }, @@ -1226,7 +1268,11 @@ namespace Barotrauma } }, () => { - return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray() }; + return new[] + { + FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(), + GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + }; }, true)); commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => @@ -1257,102 +1303,7 @@ namespace Barotrauma } } }, null, true)); - - commands.Add(new Command("upgradeitem", "upgradeitem [upgrade] [level] [items]: Adds an upgrade to the current targeted item.", args => - { - if (args.Length > 0) - { - int level; - if (args.Length > 1) - { - if (int.TryParse(args[1], out int result)) - { - level = result; - } - else - { - ThrowError($"\"{args[1]}\" is not a valid level."); - return; - } - - } - else - { - ThrowError("Parameter \"level\" is required."); - return; - } - var upgradePrefab = UpgradePrefab.Find(args[0].ToIdentifier()); - - if (upgradePrefab == null) - { - ThrowError($"Unknown upgrade: {args[0]}."); - return; - } - - List targetItems = new List(); - - if (upgradePrefab.IsWallUpgrade) - { - targetItems.AddRange(Submarine.MainSub.GetWalls(true).Cast()); - } - else - { - if (args.Length > 2) - { - targetItems.AddRange(Item.ItemList.Where(item => item.Submarine == Submarine.MainSub).Where(item => item.HasTag(args[2])).Cast()); - } - else - { - ThrowError("Argument \"tag\" is required."); - return; - } - } - - if (!targetItems.Any()) - { - ThrowError("No valid items found."); - return; - } - - foreach (MapEntity targetItem in targetItems) - { - Upgrade existingUpgrade = targetItem.GetUpgrade(args[0].ToIdentifier()); - - if (!(targetItem is ISerializableEntity sEntity)) { continue; } - - var upgrade = new Upgrade(sEntity, upgradePrefab, level); - if (targetItem.AddUpgrade(upgrade, true)) - { - if (existingUpgrade == null) - { - NewMessage($"Added {upgradePrefab.Identifier}:{level} to {sEntity.Name}.", Color.Green); - upgrade.ApplyUpgrade(); - } - else - { - NewMessage($"Set {sEntity.Name}'s {upgradePrefab.Identifier} upgrade to level {existingUpgrade.Level}.", Color.Cyan); - existingUpgrade.ApplyUpgrade(); - } - } - else - { - ThrowError($"{upgrade.Prefab.Identifier} cannot be applied to {sEntity.Name}"); - } - } - } - else - { - ThrowError("Parameter \"upgrade\" is required."); - } - }, () => - { - return new[] - { - UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray() - }; - }, true)); - commands.Add(new Command("maxupgrades", "maxupgrades [category] [prefab]: Maxes out all upgrades or only specific one if given arguments.", args => { UpgradeManager upgradeManager = GameMain.GameSession?.Campaign?.UpgradeManager; @@ -1705,6 +1656,8 @@ namespace Barotrauma }, isCheat: false)); commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); + + commands.Add(new Command("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); })); commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) => { @@ -2287,7 +2240,7 @@ namespace Barotrauma } } - public static void ShowError(string msg, Color? color = null) + public static void LogError(string msg, Color? color = null) { color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); @@ -2315,7 +2268,7 @@ namespace Barotrauma { NewMessage(msg, color.Value, isCommand: false, isError: false); } -#if DEBUG +#if DEBUG && CLIENT Console.WriteLine(msg); #endif } @@ -2459,7 +2412,7 @@ namespace Barotrauma } #endif - ShowError(error); + LogError(error); } public static void AddWarning(string warning) @@ -2500,7 +2453,7 @@ namespace Barotrauma #endif fileName += DateTime.Now.ToShortDateString() + "_" + DateTime.Now.ToShortTimeString(); - var invalidChars = Path.GetInvalidFileNameChars(); + var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform(); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); @@ -2533,9 +2486,12 @@ namespace Barotrauma #if CLIENT GameMain.DebugDraw = false; GameMain.LightManager.LightingEnabled = true; + Character.DebugDrawInteract = false; #endif Hull.EditWater = false; Hull.EditFire = false; + EnemyAIController.DisableEnemyAI = false; + HumanAIController.DisableCrewAI = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs index 05f27d592..ae088e51c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index 1aca440b3..ca7fcad9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -1,8 +1,6 @@ #nullable enable -using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -20,6 +18,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] public bool AllowLimbAfflictions { get; set; } + [Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction")] + public float MinStrength { get; set; } + public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() @@ -32,14 +33,17 @@ namespace Barotrauma if (target.CharacterHealth == null) { continue; } if (TargetLimb == LimbType.None) { - if (target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions) != null) { return true; } + var affliction = target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions); + if (affliction != null && affliction.Strength >= MinStrength) { return true; } } IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => { - LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; - if (limbType == null) { return false; } - - return limbType == TargetLimb || true; + if (affliction.Prefab.LimbSpecific) + { + LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; + if (limbType == null || limbType != TargetLimb) { return false; } + } + return affliction.Strength >= MinStrength; }); if (afflictions.Any(a => a.Identifier == Identifier)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs new file mode 100644 index 000000000..cce7bd22c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -0,0 +1,68 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + class CheckConditionalAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + private PropertyConditional Conditional { get; } + + public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (TargetTag.IsEmpty) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed."); + } + foreach (var attribute in element.Attributes()) + { + if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute)) + { + Conditional = new PropertyConditional(attribute); + break; + } + } + if (Conditional == null) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); + } + + static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag"; + } + + private string GetEventName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + + protected override bool? DetermineSuccess() + { + ISerializableEntity target = null; + if (!TargetTag.IsEmpty) + { + foreach (var t in ParentEvent.GetTargets(TargetTag)) + { + if (t is ISerializableEntity e) + { + target = e; + break; + } + } + } + if (target == null) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); + } + if (target == null || Conditional == null) + { + return true; + } + if (target is Item item) + { + return item.ConditionalMatches(Conditional); + } + return Conditional.Matches(target); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs new file mode 100644 index 000000000..56f6b82e2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs @@ -0,0 +1,59 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma; + +class CheckConnectionAction : BinaryOptionAction +{ + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ConnectionName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ConnectedItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OtherConnectionName { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MinAmount { get; set; } + + public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + int amount = 0; + var connectTargets = !ConnectedItemTag.IsEmpty ? ParentEvent.GetTargets(ConnectedItemTag) : Enumerable.Empty(); + foreach (var target in ParentEvent.GetTargets(ItemTag)) + { + if (target is not Item targetItem) { continue; } + if (targetItem.GetComponent() is not ConnectionPanel panel) { continue; } + if (panel.Connections == null || panel.Connections.None()) { continue; } + foreach (var connection in panel.Connections) + { + if (!IsCorrectConnection(connection, ConnectionName)) { continue; } + if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty) + { + amount += connection.Wires.Count(); + if (amount >= MinAmount) { return true; } + continue; + } + foreach (var wire in connection.Wires) + { + if (wire.OtherConnection(connection) is not Connection otherConnection) { continue; } + if (!ConnectedItemTag.IsEmpty && !IsCorrectConnection(otherConnection, OtherConnectionName)) { continue; } + if (!ConnectedItemTag.IsEmpty && !IsCorrectItem()) { continue; } + amount++; + if (amount >= MinAmount) { return true; } + bool IsCorrectItem() => connectTargets.Contains(otherConnection.Item); + } + + bool IsCorrectConnection(Connection connection, Identifier id) => connection.Name.ToIdentifier() == id; + } + } + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index 92ff33999..47c205aee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -36,15 +35,32 @@ namespace Barotrauma } } + public CheckDataAction(ContentXElement element, string parentDebugString) : base(null, element) + { + if (string.IsNullOrEmpty(Condition)) + { + Condition = element.GetAttributeString("value", string.Empty)!; + if (string.IsNullOrEmpty(Condition)) + { + DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element})."); + } + } + } + + public bool GetSuccess() + { + return DetermineSuccess() ?? false; + } + protected override bool? DetermineSuccess() { - if (!(GameMain.GameSession?.GameMode is CampaignMode campaignMode)) { return false; } + if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; } string[] splitString = Condition.Split(' '); - string value = Condition; + string value; if (splitString.Length > 0) { - #warning Is this correct? + //the first part of the string is the operator, skip it value = string.Join(" ", splitString.Skip(1)); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index b7874d26a..9413193d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Items.Components; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -14,6 +15,20 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string ItemTags { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first target when the check succeeds.")] + public Identifier ApplyTagToTarget { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool RequireEquipped { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int ItemContainerIndex { get; set; } + + private readonly IReadOnlyList conditionals; private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; @@ -22,6 +37,19 @@ namespace Barotrauma { itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers(); itemTags = ItemTags.Split(",").ToIdentifiers(); + var conditionalList = new List(); + foreach (ContentXElement subElement in element.GetChildElements("conditional")) + { + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + conditionalList.Add(new PropertyConditional(attribute)); + } + } + break; + } + conditionals = conditionalList; } protected override bool? DetermineSuccess() @@ -30,23 +58,72 @@ namespace Barotrauma if (!targets.Any()) { return null; } foreach (var target in targets) { - if (!(target is Character chr)) { continue; } - if (chr.Inventory == null) { continue; } - - if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; } - - foreach (var identifier in itemIdentifierSplit) + if (target is Character character) { - if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null) + Inventory inventory = character.Inventory; + if (CheckInventory(character.Inventory, character)) { + if (!ApplyTagToTarget.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToTarget, target); + } return true; } } + else if (target is Item item) + { + int i = 0; + foreach (var itemContainer in item.GetComponents()) + { + if (ItemContainerIndex == -1 || i == ItemContainerIndex) + { + if (CheckInventory(itemContainer.Inventory, character: null)) + { + if (!ApplyTagToTarget.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToTarget, target); + } + return true; + } + } + i++; + } + } } - return false; } + private bool CheckInventory(Inventory inventory, Character character) + { + if (inventory == null) { return false; } + int count = 0; + foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier))) + { + if (!ConditionalsMatch(item, character)) { continue; } + count++; + if (count >= Amount) { return true; } + } + return false; + } + + private bool ConditionalsMatch(Item item, Character character = null) + { + if (item == null) { return false; } + foreach (PropertyConditional conditional in conditionals) + { + if (!conditional.Matches(item)) + { + return false; + } + } + if (RequireEquipped) + { + if (character == null) { return false; } + return character.HasEquippedItem(item); + } + return true; + } + public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs new file mode 100644 index 000000000..6723d3abb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -0,0 +1,55 @@ +namespace Barotrauma +{ + class CheckOrderAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderOption { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderTargetTag { get; set; } + + public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + Character targetCharacter = null; + if (!TargetTag.IsEmpty) + { + foreach (var t in ParentEvent.GetTargets(TargetTag)) + { + if (t is Character c) + { + targetCharacter = c; + break; + } + } + } + if (targetCharacter == null) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + return false; + } + var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority(); + if (currentOrderInfo?.Identifier == OrderIdentifier) + { + if (!OrderTargetTag.IsEmpty) + { + if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; } + } + return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption; + } + return false; + } + + private string GetEventName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs new file mode 100644 index 000000000..6f1cb5867 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -0,0 +1,89 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; + +namespace Barotrauma +{ + class CheckSelectedItemAction : BinaryOptionAction + { + public enum SelectedItemType { Primary, Secondary, Any }; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CharacterTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] + public SelectedItemType ItemType { get; set; } + + public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + Character character = null; + if (!CharacterTag.IsEmpty) + { + foreach (var t in ParentEvent.GetTargets(CharacterTag)) + { + if (t is Character c) + { + character = c; + break; + } + } + } + if (character == null) + { + DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail."); + return false; + } + if (!TargetTag.IsEmpty) + { + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + if (targets.None()) + { + DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + return false; + } + foreach (var target in targets) + { + if (target is not Item targetItem) + { + continue; + } + if (IsSelected(targetItem)) + { + return true; + } + } + return false; + + bool IsSelected(Item item) + { + return ItemType switch + { + SelectedItemType.Any => character.IsAnySelectedItem(item), + SelectedItemType.Primary => character.SelectedItem == item, + SelectedItemType.Secondary => character.SelectedSecondaryItem == item, + _ => false + }; + } + } + else + { + return ItemType switch + { + SelectedItemType.Any => !character.HasSelectedAnyItem, + SelectedItemType.Primary => character.SelectedItem == null, + SelectedItemType.Secondary => character.SelectedSecondaryItem == null, + _ => false + }; + } + } + + private string GetEventName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs new file mode 100644 index 000000000..6de01c0e8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs @@ -0,0 +1,94 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; + +namespace Barotrauma +{ + class CheckSelectedAction : BinaryOptionAction + { + public enum SelectedItemType { Primary, Secondary, Any }; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CharacterTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] + public SelectedItemType ItemType { get; set; } + + public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + Character character = null; + if (!CharacterTag.IsEmpty) + { + foreach (var t in ParentEvent.GetTargets(CharacterTag)) + { + if (t is Character c) + { + character = c; + break; + } + } + } + if (character == null) + { + DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail."); + return false; + } + if (!TargetTag.IsEmpty) + { + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + if (targets.None()) + { + DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + return false; + } + foreach (var target in targets) + { + if (target is Character targetCharacter) + { + if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; } + continue; + } + if (target is not Item targetItem) + { + continue; + } + if (IsSelected(targetItem)) + { + return true; + } + } + return false; + + bool IsSelected(Item item) + { + return ItemType switch + { + SelectedItemType.Any => character.IsAnySelectedItem(item), + SelectedItemType.Primary => character.SelectedItem == item, + SelectedItemType.Secondary => character.SelectedSecondaryItem == item, + _ => false + }; + } + } + else + { + return ItemType switch + { + SelectedItemType.Any => !character.HasSelectedAnyItem, + SelectedItemType.Primary => character.SelectedItem == null, + SelectedItemType.Secondary => character.SelectedSecondaryItem == null, + _ => false + }; + } + } + + private string GetEventName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs new file mode 100644 index 000000000..b974c6f83 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs @@ -0,0 +1,49 @@ +#nullable enable + +namespace Barotrauma +{ + internal sealed class CheckTalentAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TalentIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + if (TargetTag.IsEmpty) + { + return false; + } + + Character? matchingCharacter = null; + + foreach (Entity entity in ParentEvent.GetTargets(TargetTag)) + { + if (entity is Character character) + { + matchingCharacter = character; + break; + } + } + + return matchingCharacter is not null && matchingCharacter.HasTalent(TalentIdentifier); + } + + public override string ToDebugString() + { + string subActionStr = ""; + if (succeeded.HasValue) + { + subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; + } + + return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckTalentAction)} -> (Talent: {TalentIdentifier.ColorizeObject()}" + + $" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + + subActionStr; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 27748f54d..a438db9a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,7 +59,11 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } - private Character speaker; + public Character speaker + { + get; + private set; + } private AIObjective prevIdleObjective, prevGotoObjective; @@ -120,7 +124,7 @@ namespace Barotrauma #else foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.InGame && c.Character != null) { ServerWrite(speaker, c); } + if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); } } #endif ResetSpeaker(); @@ -331,9 +335,11 @@ namespace Barotrauma if (!TargetTag.IsEmpty) { targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); - if (!targets.Any() || IsBlockedByAnotherConversation(targets)) { return; } + if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; } } + if (targetCharacter != null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) { return; } + if (speaker?.AIController is HumanAIController humanAI) { prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index b01ede512..c0fc93a5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -134,7 +134,7 @@ namespace Barotrauma public static EventAction Instantiate(ScriptedEvent scriptedEvent, ContentXElement element) { - Type actionType = null; + Type actionType; try { actionType = Type.GetType("Barotrauma." + element.Name, true, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs index 47ff662f2..e9995190f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - namespace Barotrauma { class GodModeAction : EventAction @@ -10,6 +5,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes)] public bool Enabled { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character's active afflictions be updated (e.g. applying visual effects of the afflictions)")] + public bool UpdateAfflictions { get; set; } + [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -35,9 +33,16 @@ namespace Barotrauma { if (target != null && target is Character character) { - character.GodMode = Enabled; + if (UpdateAfflictions) + { + character.CharacterHealth.Unkillable = Enabled; + } + else + { + character.GodMode = Enabled; + } } - } + } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs new file mode 100644 index 000000000..7eecbbbb2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs @@ -0,0 +1,33 @@ +namespace Barotrauma; + +partial class InventoryHighlightAction : EventAction +{ + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int ItemContainerIndex { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool Recursive { get; set; } + + private bool isFinished; + + public InventoryHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs new file mode 100644 index 000000000..2834b8b84 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -0,0 +1,77 @@ +namespace Barotrauma +{ + partial class MessageBoxAction : EventAction + { + public enum ActionType { Create, ConnectObjective, Close, Clear } + + [Serialize(ActionType.Create, IsPropertySaveable.Yes)] + public ActionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Tag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Header { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Text { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string IconStyle { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool HideCloseButton { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string CloseOnInput { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnSelectTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnPickUpTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnEquipTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnExitRoomName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnInRoomName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ObjectiveTag { get; set; } + + private bool isFinished = false; + + public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Identifier.IsEmpty) + { + Identifier = element.GetAttributeIdentifier("id", Identifier.Empty); + } + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; + + public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MessageBoxAction)}"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index d4d95ebbb..2c298853d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -55,7 +55,7 @@ namespace Barotrauma if (GameMain.GameSession.GameMode is CampaignMode campaign) { - MissionPrefab prefab = null; + Mission unlockedMission = null; var unlockLocation = FindUnlockLocation(); if (unlockLocation == null && CreateLocationIfNotFound) { @@ -72,27 +72,34 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } else if (!MissionTag.IsEmpty) { - prefab = unlockLocation.UnlockMissionByTag(MissionTag); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag); } if (campaign is MultiPlayerCampaign mpCampaign) { mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); } - if (prefab != null) + if (unlockedMission != null) { - DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\"."); - #if CLIENT - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) { - IconColor = prefab.IconColor + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); + } + else + { + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].Name}\" to \"{unlockedMission.Locations[1].Name}\"."); + } +#if CLIENT + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", unlockedMission.Name), + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: unlockedMission.Prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + { + IconColor = unlockedMission.Prefab.IconColor }; - #else - NotifyMissionUnlock(prefab); +#else + NotifyMissionUnlock(unlockedMission); #endif } } @@ -138,16 +145,17 @@ namespace Barotrauma { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } - + #if SERVER - private void NotifyMissionUnlock(MissionPrefab prefab) + private void NotifyMissionUnlock(Mission mission) { foreach (Client client in GameMain.Server.ConnectedClients) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.EVENTACTION); - outmsg.Write((byte)EventManager.NetworkEventType.MISSION); - outmsg.Write(prefab.Identifier); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); + outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteString(mission.Name.Value); GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 90e6eb540..143563584 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -16,6 +16,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool RemoveFromCrew { get; set; } + private bool isFinished = false; public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -35,34 +38,47 @@ namespace Barotrauma if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; - GameMain.GameSession.CrewManager.AddCharacter(npc); + ChangeItemTeam(Submarine.MainSub, true); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); + } + } + else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2)) + { + npc.Info.StartItemsGiven = true; + GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true); + var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamTag); + ChangeItemTeam(sub, false); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamTag, npc.Inventory.AllItems)); + } + } + + void ChangeItemTeam(Submarine sub, bool allowStealing) + { foreach (Item item in npc.Inventory.AllItems) { - item.AllowStealing = true; - var wifiComponent = item.GetComponent(); - if (wifiComponent != null) + item.AllowStealing = allowStealing; + if (item.GetComponent() is { } wifiComponent) { wifiComponent.TeamID = TeamTag; } - var idCard = item.GetComponent(); - if (idCard != null) + if (item.GetComponent() is { } idCard) { idCard.TeamID = TeamTag; idCard.SubmarineSpecificID = 0; } } - - WayPoint subWaypoint = - WayPoint.WayPointList.Find(wp => wp.Submarine == Submarine.MainSub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info.Job?.Prefab) ?? - WayPoint.WayPointList.Find(wp => wp.Submarine == Submarine.MainSub && wp.SpawnType == SpawnType.Human); + WayPoint subWaypoint = + WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info.Job?.Prefab) ?? + WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); } -#if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); -#endif } } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 7cc2f28ef..cee49e531 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -1,8 +1,5 @@ -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -17,6 +14,12 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes)] public bool Follow { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxTargets { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool AbandonOnReset { get; set; } + private bool isFinished = false; public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -32,6 +35,7 @@ namespace Barotrauma target = ParentEvent.GetTargets(TargetTag).FirstOrDefault(); if (target == null) { return; } + int targetCount = 0; affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { @@ -56,6 +60,11 @@ namespace Barotrauma } } } + targetCount++; + if (MaxTargets > -1 && targetCount >= MaxTargets) + { + break; + } } isFinished = true; } @@ -67,11 +76,11 @@ namespace Barotrauma public override void Reset() { - if (affectedNpcs != null && target != null) + if (affectedNpcs != null && target != null && AbandonOnReset) { foreach (var npc in affectedNpcs) { - if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; } foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) { if (goToObjective.Target == target) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs new file mode 100644 index 000000000..b39eb465f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class NPCOperateItemAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemComponentName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderOption { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool RequireEquip { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool Operate { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxTargets { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool AbandonOnReset { get; set; } + + private bool isFinished = false; + + public NPCOperateItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + + private List affectedNpcs = null; + private Item target = null; + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + target = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Item; + if (target == null) { return; } + + int targetCount = 0; + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); + foreach (var npc in affectedNpcs) + { + if (npc.AIController is not HumanAIController humanAiController) { continue; } + + if (Operate) + { + ItemComponentName = "Controller".ToIdentifier(); + var itemComponent = target.Components.FirstOrDefault(ic => ItemComponentName == ic.Name); + if (itemComponent == null) + { + DebugConsole.AddWarning($"Error in NPCOperateItemAction: could not find the component \"{ItemComponentName}\" in item \"{target.Name}\"."); + } + else + { + var newObjective = new AIObjectiveOperateItem(itemComponent, npc, humanAiController.ObjectiveManager, OrderOption, RequireEquip) + { + OverridePriority = 100.0f + }; + humanAiController.ObjectiveManager.AddObjective(newObjective); + humanAiController.ObjectiveManager.WaitTimer = 0.0f; + humanAiController.ObjectiveManager.Objectives.RemoveAll(o => o is AIObjectiveGoTo gotoOjective); + } + } + else + { + foreach (var objective in humanAiController.ObjectiveManager.Objectives) + { + if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.OperateTarget == target) + { + objective.Abandon = true; + } + } + } + targetCount++; + if (MaxTargets > -1 && targetCount >= MaxTargets) + { + break; + } + } + isFinished = true; + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + if (affectedNpcs != null && target != null && AbandonOnReset) + { + foreach (var npc in affectedNpcs) + { + if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; } + foreach (var operateItemObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) + { + if (operateItemObjective.OperateTarget == target) + { + operateItemObjective.Abandon = true; + } + } + } + target = null; + affectedNpcs = null; + } + isFinished = false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AIObjectiveOperateItem)} -> (NPCTag: {NPCTag.ColorizeObject()}, TargetTag: {TargetTag.ColorizeObject()}, Operate: {Operate.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index aa2bee231..fa3b3d2f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -29,7 +29,7 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Wait) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || !(npc.AIController is HumanAIController)) { continue; } + if (npc.Removed || npc.AIController is not HumanAIController) { continue; } if (gotoObjective != null) { gotoObjective.Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 70f6a7b61..657c6af21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -57,6 +57,12 @@ namespace Barotrauma private readonly HashSet targetModuleTags = new HashSet(); + [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")] + public bool AllowDuplicates { get; set; } + + [Serialize(100.0f, IsPropertySaveable.Yes)] + public float Offset { get; set; } + [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] public string TargetModuleTags { @@ -115,10 +121,16 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); if (humanPrefab != null) { + if (!AllowDuplicates && + Character.CharacterList.Any(c => c.Info?.HumanPrefabIds.NpcIdentifier == NPCIdentifier && c.Info?.HumanPrefabIds.NpcSetIdentifier == NPCSetIdentifier)) + { + spawned = true; + return; + } ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; @@ -145,10 +157,15 @@ namespace Barotrauma } else if (!SpeciesName.IsEmpty) { + if (!AllowDuplicates && Character.CharacterList.Any(c => c.SpeciesName == SpeciesName)) + { + spawned = true; + return; + } ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter => { if (!TargetTag.IsEmpty && newCharacter != null) { @@ -194,7 +211,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 377fa8eb0..208a7fbfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,10 +1,7 @@ using Barotrauma.Extensions; using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -36,6 +33,7 @@ namespace Barotrauma ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), ("structureidentifier", TagStructuresByIdentifier), + ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), ("hullname", TagHullsByName) @@ -100,6 +98,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } + private void TagStructuresBySpecialTag(Identifier tag) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + } + private void TagItemsByIdentifier(Identifier identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs new file mode 100644 index 000000000..1aacece60 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs @@ -0,0 +1,48 @@ +namespace Barotrauma; + +class TeleportAction : EventAction +{ + public enum TeleportPosition { MainSub, Outpost } + + [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes)] + public TeleportPosition Position { get; set; } + + [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + public SpawnType SpawnType { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string SpawnPointTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + private bool isFinished; + + public TeleportAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + Submarine sub = Position switch + { + TeleportPosition.MainSub => Submarine.MainSub, + TeleportPosition.Outpost => GameMain.GameSession?.Level?.StartOutpost, + _ => null + }; + if (WayPoint.GetRandom(spawnType: SpawnType, sub: sub, spawnPointTag: SpawnPointTag) is WayPoint wp) + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is Character c) + { + c.TeleportTo(wp.WorldPosition); + } + } + } + isFinished = true; + } + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 09a689776..385dc13ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -157,8 +157,9 @@ namespace Barotrauma npcsOrItems.Add(item); } item.CampaignInteractionType = CampaignMode.InteractionType.Examine; - if (player.SelectedConstruction == item || - player.Inventory != null && player.Inventory.Contains(item) || + if (player.SelectedItem == item || + player.SelectedSecondaryItem == item || + (player.Inventory != null && player.Inventory.Contains(item)) || (player.FocusedItem == item && player.IsKeyHit(InputType.Use))) { Trigger(e1, e2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs new file mode 100644 index 000000000..9544f9371 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs @@ -0,0 +1,32 @@ +namespace Barotrauma +{ + class TutorialCompleteAction : EventAction + { + private bool isFinished; + + public TutorialCompleteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + tutorialMode.Tutorial?.Complete(); + } +#endif + isFinished = true; + } + + public override bool IsFinished(ref string goToLabel) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs new file mode 100644 index 000000000..eebac860d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs @@ -0,0 +1,27 @@ +namespace Barotrauma; + +partial class TutorialHighlightAction : EventAction +{ + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool State { get; set; } + + private bool isFinished; + + public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs new file mode 100644 index 000000000..a47d4e932 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs @@ -0,0 +1,65 @@ +using System.Linq; + +namespace Barotrauma; + +class TutorialIconAction : EventAction +{ + public enum ActionType { Add, Remove, RemoveTarget, RemoveIcon, Clear }; + + [Serialize(ActionType.Add, IsPropertySaveable.Yes)] + public ActionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier IconStyle { get; set; } + + private bool isFinished; + + public TutorialIconAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + if (ParentEvent.GetTargets(TargetTag).FirstOrDefault() is Entity target) + { + if (Type == ActionType.Add) + { + tutorialMode.Tutorial?.Icons.Add((target, IconStyle)); + } + else if(Type == ActionType.Remove) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target && i.iconStyle == IconStyle); + } + else if (Type == ActionType.RemoveTarget) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target); + } + else if (Type == ActionType.RemoveIcon) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.iconStyle == IconStyle); + } + else if (Type == ActionType.Clear) + { + tutorialMode.Tutorial?.Icons.Clear(); + } + } + } +#endif + isFinished = true; + } + + public override bool IsFinished(ref string goToLabel) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs new file mode 100644 index 000000000..587fb20a7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs @@ -0,0 +1,54 @@ +namespace Barotrauma +{ + partial class TutorialSegmentAction : EventAction + { + public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove }; + + [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] + public SegmentActionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ObjectiveTag { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool AutoPlayVideo { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TextTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string VideoFile { get; set; } + + [Serialize(450, IsPropertySaveable.Yes)] + public int Width { get; set; } + + [Serialize(80, IsPropertySaveable.Yes)] + public int Height { get; set; } + + private bool isFinished; + + public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Identifier.IsEmpty) + { + Identifier = element.GetAttributeIdentifier("id", Identifier.Empty); + } + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs new file mode 100644 index 000000000..b990fdf66 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -0,0 +1,62 @@ +namespace Barotrauma; + +partial class UIHighlightAction : EventAction +{ + public enum ElementId + { + None, + RepairButton, + PumpSpeedSlider, + PassiveSonarIndicator, + ActiveSonarIndicator, + SonarModeSwitch, + DirectionalSonarFrame, + SteeringModeSwitch, + MaintainPosTickBox, + AutoTempSwitch, + PowerButton, + FissionRateSlider, + TurbineOutputSlider, + DeconstructButton, + RechargeSpeedSlider, + CPRButton + } + + [Serialize(ElementId.None, IsPropertySaveable.Yes)] + public ElementId Id { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier EntityIdentifier { get; set; } + + [Serialize(OrderCategory.Emergency, IsPropertySaveable.Yes)] + public OrderCategory OrderCategory { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderOption { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OrderTargetTag { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool Bounce { get; set; } + + private bool isFinished; + + public UIHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index ac2b2fe9f..a69ee509a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -55,9 +55,9 @@ namespace Barotrauma foreach (Client client in GameMain.Server.ConnectedClients) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.EVENTACTION); - outmsg.Write((byte)EventManager.NetworkEventType.UNLOCKPATH); - outmsg.Write((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection)); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.UNLOCKPATH); + outmsg.WriteUInt16((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection)); GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 63dd365ef..8ecedce89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -71,6 +71,10 @@ namespace Barotrauma private readonly List activeEvents = new List(); + private readonly HashSet finishedEvents = new HashSet(); + private readonly HashSet nonRepeatableEvents = new HashSet(); + + #if DEBUG && SERVER private DateTime nextIntensityLogTime; #endif @@ -169,54 +173,48 @@ namespace Barotrauma CreateEvents(additiveSet); } - if (level?.LevelData?.Type == LevelData.LevelType.Outpost) + if (level?.LevelData != null) { - //if the outpost is connected to a locked connection, create an event to unlock it - if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => - e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData.Biome.Identifier); + //if the outpost is connected to a locked connection, create an event to unlock it + if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) + { + var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); + var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => + e.BiomeIdentifier.IsEmpty || + e.BiomeIdentifier == level.LevelData.Biome.Identifier); - var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); - if (unlockPathEventPrefab != null) - { - var newEvent = unlockPathEventPrefab.CreateInstance(); - newEvent.Init(); - ActiveEvents.Add(newEvent); - } - else - { - //if no event that unlocks the path can be found, unlock it automatically - level.StartLocation.Connections.ForEach(c => c.Locked = false); - } - } - - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); - if (level.LevelData.EventHistory.Count > MaxEventHistory) - { - level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); - } - AddChildEvents(initialEventSet); - void AddChildEvents(EventSet eventSet) - { - if (eventSet == null) { return; } - if (eventSet.OncePerOutpost) - { - foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) + var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? + ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : + ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); + if (unlockPathEventPrefab != null) { - if (!level.LevelData.NonRepeatableEvents.Contains(ep)) - { - level.LevelData.NonRepeatableEvents.Add(ep); - } + var newEvent = unlockPathEventPrefab.CreateInstance(); + ActiveEvents.Add(newEvent); + } + else + { + //if no event that unlocks the path can be found, unlock it automatically + level.StartLocation.Connections.ForEach(c => c.Locked = false); } } - foreach (EventSet childSet in eventSet.ChildSets) + + AddChildEvents(initialEventSet); + void AddChildEvents(EventSet eventSet) { - AddChildEvents(childSet); + if (eventSet == null) { return; } + if (eventSet.OncePerOutpost) + { + foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) + { + nonRepeatableEvents.Add(ep); + } + } + foreach (EventSet childSet in eventSet.ChildSets) + { + AddChildEvents(childSet); + } } } } @@ -224,6 +222,7 @@ namespace Barotrauma PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; + eventsInitialized = false; isCrewAway = false; crewAwayDuration = 0.0f; crewAwayResetTimer = 0.0f; @@ -350,13 +349,33 @@ namespace Barotrauma selectedEvents.Clear(); activeEvents.Clear(); QueuedEvents.Clear(); + finishedEvents.Clear(); + nonRepeatableEvents.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); + pathFinder = null; } + /// + /// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history + /// + public void RegisterEventHistory() + { + level.LevelData.EventsExhausted = true; + if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) + { + level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); + if (level.LevelData.EventHistory.Count > MaxEventHistory) + { + level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); + } + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + } + } + public void SkipEventCooldown() { eventCoolDown = 0.0f; @@ -375,6 +394,8 @@ namespace Barotrauma selectedEvents.Remove(eventSet); if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } + if (eventSet.Exhaustible && level.LevelData.EventsExhausted) { return; } + DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); int applyCount = 1; @@ -427,7 +448,8 @@ namespace Barotrauma if (suitablePrefabSubsets.Any()) { var unusedEvents = suitablePrefabSubsets.ToList(); - for (int j = 0; j < eventSet.EventCount; j++) + int eventCount = eventSet.GetEventCount(level); + for (int j = 0; j < eventCount; j++) { if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); @@ -438,7 +460,6 @@ namespace Barotrauma var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(eventSet); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -468,8 +489,6 @@ namespace Barotrauma var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(eventSet); - DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -592,12 +611,25 @@ namespace Barotrauma return true; } + private bool eventsInitialized; public void Update(float deltaTime) { if (!Enabled || level == null) { return; } if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; } + if (!eventsInitialized) + { + foreach (var eventSet in selectedEvents.Keys) + { + foreach (var ev in selectedEvents[eventSet]) + { + ev.Init(eventSet); + } + } + eventsInitialized = true; + } + //clients only calculate the intensity but don't create any events //(the intensity is used for controlling the background music) CalculateCurrentIntensity(deltaTime); @@ -706,7 +738,18 @@ namespace Barotrauma foreach (Event ev in activeEvents) { - if (!ev.IsFinished) { ev.Update(deltaTime); } + if (!ev.IsFinished) + { + ev.Update(deltaTime); + } + else if (!finishedEvents.Contains(ev)) + { + if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) + { + if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); } + } + finishedEvents.Add(ev); + } } if (QueuedEvents.Count > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 3c2be4201..9b397d4be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -24,7 +24,7 @@ namespace Barotrauma } #endif - class EventSet : Prefab + sealed class EventSet : Prefab { internal class EventDebugStats { @@ -92,7 +92,13 @@ namespace Barotrauma public readonly bool ChooseRandom; - public readonly int EventCount = 1; + private readonly int eventCount = 1; + private readonly Dictionary overrideEventCount = new Dictionary(); + + /// + /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. + /// + public readonly bool Exhaustible; public readonly float MinDistanceTraveled; public readonly float MinMissionTime; @@ -250,7 +256,8 @@ namespace Barotrauma MaxIntensity = Math.Max(element.GetAttributeFloat("maxintensity", 100.0f), MinIntensity); ChooseRandom = element.GetAttributeBool("chooserandom", false); - EventCount = element.GetAttributeInt("eventcount", 1); + eventCount = element.GetAttributeInt("eventcount", 1); + Exhaustible = element.GetAttributeBool("exhaustible", false); MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); @@ -288,6 +295,13 @@ namespace Barotrauma case "eventset": childSets.Add(new EventSet(subElement, file, this)); break; + case "overrideeventcount": + Identifier locationType = subElement.GetAttributeIdentifier("locationtype", ""); + if (!overrideEventCount.ContainsKey(locationType)) + { + overrideEventCount.Add(locationType, subElement.GetAttributeInt("eventcount", eventCount)); + } + break; default: //an element with just an identifier = reference to an event prefab if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase)) @@ -332,6 +346,12 @@ namespace Barotrauma return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; } + public int GetEventCount(Level level) + { + if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; } + return count; + } + public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); @@ -358,7 +378,7 @@ namespace Barotrauma var unusedEvents = thisSet.EventPrefabs.ToList(); if (unusedEvents.Any()) { - for (int i = 0; i < thisSet.EventCount; i++) + for (int i = 0; i < thisSet.eventCount; i++) { var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); if (eventPrefab.EventPrefabs.Any(p => p != null)) @@ -469,12 +489,6 @@ namespace Barotrauma } } - public override void Dispose() - { - foreach (var childSet in ChildSets) - { - childSet.Dispose(); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 9665b6b90..7e4da0ea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -329,21 +329,14 @@ namespace Barotrauma } - public override void End() + protected override bool DetermineCompleted() { - completed = State > 0 && State != HostagesKilledState; - if (completed) - { - if (Prefab.LocationTypeChangeOnCompleted != null) - { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } - GiveReward(); - } - else - { - failed = requireRescue.Any(r => r.Removed || r.IsDead); - } + return State > 0 && State != HostagesKilledState; + } + + protected override void EndMissionSpecific(bool completed) + { + failed = !completed && requireRescue.Any(r => r.Removed || r.IsDead); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 2ed692fa5..2bfb34391 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -156,22 +156,21 @@ namespace Barotrauma return true; } - private bool IsItemDestroyed(Item item) => item == null || item.Removed || item.Condition <= 0.0f; + private static bool IsItemDestroyed(Item item) => item == null || item.Removed || item.Condition <= 0.0f; - private bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; + private static bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; - public override void End() + protected override bool DetermineCompleted() { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); - if (State > 0 && exitingLevel) - { - GiveReward(); - completed = true; - } + return State > 0 && exitingLevel; + } + protected override void EndMissionSpecific(bool completed) + { failed = !completed && State > 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 9e3c000b3..6e46b3c5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -100,7 +100,8 @@ namespace Barotrauma if (!connectedSubs.Contains(item.Submarine)) { continue; } if (item.GetComponent() != null || item.GetComponent() != null || - item.GetComponent() != null) + item.GetComponent() != null || + item.GetComponent() != null) { item.InvulnerableToDamage = true; } @@ -162,20 +163,16 @@ namespace Barotrauma #endif } - public override void End() + protected override bool DetermineCompleted() { - completed = level.CheckBeaconActive(); - if (completed) + return level.CheckBeaconActive(); + } + + protected override void EndMissionSpecific(bool completed) + { + if (completed && level.LevelData != null) { - if (Prefab.LocationTypeChangeOnCompleted != null) - { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } - GiveReward(); - if (level.LevelData != null) - { - level.LevelData.IsBeaconActive = true; - } + level.LevelData.IsBeaconActive = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 58aae000c..7ead29659 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -219,7 +219,7 @@ namespace Barotrauma else if (sub != this.currentSub || missionsChanged) { this.currentSub = sub; - this.nextRoundSubInfo = sub.Info; + this.nextRoundSubInfo = sub?.Info; DetermineCargo(); } @@ -294,22 +294,21 @@ namespace Barotrauma } } - public override void End() + protected override bool DetermineCompleted() { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { int deliveredItemCount = items.Count(it => IsItemDelivered(it)); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { - GiveReward(); - completed = true; - if (Prefab.LocationTypeChangeOnCompleted != null) - { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } + return true; } } + return false; + } + protected override void EndMissionSpecific(bool completed) + { foreach (Item item in items) { if (!item.Removed) { item.Remove(); } @@ -318,7 +317,7 @@ namespace Barotrauma failed = !completed; } - private bool IsItemDelivered(Item item) + private static bool IsItemDelivered(Item item) { if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 9e2e4c921..917b31d90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -109,18 +109,13 @@ namespace Barotrauma subs[1].FlipX(); #if SERVER crews = new List[] { new List(), new List() }; + roundEndTimer = RoundEndDuration; #endif } - public override void End() + protected override bool DetermineCompleted() { - if (GameMain.NetworkMember == null) { return; } - - if (Winner != CharacterTeamType.None) - { - GiveReward(); - completed = true; - } + return Winner != CharacterTeamType.None; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index a1434ba45..fe7c3ff84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -95,37 +95,42 @@ namespace Barotrauma randSync = Rand.RandSync.Unsynced; } - //if any of the escortees have a job defined, try to use a spawnpoint designated for that job + List humanPrefabsToSpawn = new List(); foreach (XElement element in characterConfig.Elements()) { + int count = CalculateScalingEscortedCharacterCount(inMission: true); var humanPrefab = GetHumanPrefabFromElement(element); - if (humanPrefab == null || string.IsNullOrEmpty(humanPrefab.Job) || humanPrefab.Job.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + for (int i = 0; i < count; i++) + { + humanPrefabsToSpawn.Add(humanPrefab); + } + } - var jobPrefab = humanPrefab.GetJobPrefab(); + //if any of the escortees have a job defined, try to use a spawnpoint designated for that job + foreach (var humanPrefab in humanPrefabsToSpawn) + { + if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; } + var jobPrefab = humanPrefab.GetJobPrefab(randSync); if (jobPrefab != null) { var jobSpecificSpawnPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, Submarine.MainSub); - if (jobSpecificSpawnPos != null) + if (jobSpecificSpawnPos != null) { explicitStayInHullPos = jobSpecificSpawnPos; break; } } } - - foreach (XElement element in characterConfig.Elements()) + foreach (var humanPrefab in humanPrefabsToSpawn) { - int count = CalculateScalingEscortedCharacterCount(inMission: true); - for (int i = 0; i < count; i++) + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); + if (spawnedCharacter.AIController is HumanAIController humanAI) { - Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(element), characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); - if (spawnedCharacter.AIController is HumanAIController humanAI) - { - humanAI.InitMentalStateManager(); - } + humanAI.InitMentalStateManager(); } } + if (terroristChance > 0f) { int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); @@ -300,7 +305,7 @@ namespace Barotrauma return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier); } - public override void End() + protected override bool DetermineCompleted() { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { @@ -316,11 +321,14 @@ namespace Barotrauma if (friendliesSurvived && !terroristsSurvived && !vipDied) { - GiveReward(); - completed = true; + return true; } } + return false; + } + protected override void EndMissionSpecific(bool completed) + { if (!IsClient) { foreach (Character character in characters) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index cb8508f9d..f0fa3a328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -1,6 +1,4 @@ -using Barotrauma.Networking; - -namespace Barotrauma +namespace Barotrauma { partial class GoToMission : Mission { @@ -11,7 +9,22 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - State = 1; + if (Level.Loaded?.Type == LevelData.LevelType.Outpost) + { + State = 1; + } + } + + protected override bool DetermineCompleted() + { + if (Level.Loaded?.Type == LevelData.LevelType.Outpost) + { + return true; + } + else + { + return Submarine.MainSub is { AtEndExit: true }; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index c4a787a40..0d136f64f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -3,7 +3,9 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using PositionType = Barotrauma.Level.PositionType; namespace Barotrauma { @@ -29,6 +31,25 @@ namespace Barotrauma private readonly HashSet caves = new HashSet(); + private readonly PositionType positionType = PositionType.Cave; + /// + /// The list order is important. + /// It defines the order in which we "override" in case no valid position types are found + /// in the level when generating them in . + /// + public static readonly ImmutableArray ValidPositionTypes = new PositionType[] + { + PositionType.Cave, + PositionType.SidePath, + PositionType.MainPath, + PositionType.AbyssCave, + }.ToImmutableArray(); + + /// + /// Percentage. Value between 0 and 1. + /// + private readonly float resourceHandoverAmount; + public override IEnumerable SonarPositions { get @@ -39,8 +60,23 @@ namespace Barotrauma } } + public override LocalizedString SuccessMessage => ModifyMessage(base.SuccessMessage); + public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); + public override LocalizedString Description => ModifyMessage(description); + public override LocalizedString Name => ModifyMessage(base.Name, false); + public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false); + public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { + var positionType = prefab.ConfigElement.GetAttributeEnum("PositionType", in this.positionType); + if (ValidPositionTypes.Contains(positionType)) + { + this.positionType = positionType; + } + + float handoverAmount = prefab.ConfigElement.GetAttributeFloat("ResourceHandoverAmount", 0.0f); + resourceHandoverAmount = Math.Clamp(handoverAmount, 0.0f, 1.0f); + var configElement = prefab.ConfigElement.GetChildElement("Items"); foreach (var c in configElement.GetChildElements("Item")) { @@ -92,27 +128,28 @@ namespace Barotrauma caves.Clear(); if (IsClient) { return; } - foreach (var kvp in resourceClusters) + + foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) { - var prefab = ItemPrefab.Find(null, kvp.Key); - if (prefab == null) + if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { - DebugConsole.ThrowError("Error in MineralMission - " + - "couldn't find an item prefab with the identifier " + kvp.Key); + DebugConsole.ThrowError($"Error in MineralMission: couldn't find an item prefab (identifier: \"{identifier}\")"); continue; } - var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.Amount, out float rotation); - if (spawnedResources.Count < kvp.Value.Amount) - { - DebugConsole.ThrowError("Error in MineralMission - " + - "spawned " + spawnedResources.Count + "/" + kvp.Value.Amount + " of " + prefab.Name); - } - if (spawnedResources.None()) { continue; } - this.spawnedResources.Add(kvp.Key, spawnedResources); - foreach (Level.Cave cave in Level.Loaded.Caves) + var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves); + if (spawnedResources.Count < cluster.Amount) { - foreach (Item spawnedResource in spawnedResources) + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}"); + } + + if (spawnedResources.None()) { continue; } + + this.spawnedResources.Add(identifier, spawnedResources); + + foreach (var cave in Level.Loaded.Caves) + { + foreach (var spawnedResource in spawnedResources) { if (cave.Area.Contains(spawnedResource.WorldPosition)) { @@ -123,6 +160,7 @@ namespace Barotrauma } } } + CalculateMissionClusterPositions(); FindRelevantLevelResources(); } @@ -143,16 +181,38 @@ namespace Barotrauma } } - public override void End() + protected override bool DetermineCompleted() { - if (EnoughHaveBeenCollected()) + return EnoughHaveBeenCollected(); + } + + protected override void EndMissionSpecific(bool completed) + { + failed = !completed && state > 0; + if (completed) { - if (Prefab.LocationTypeChangeOnCompleted != null) + if (!IsClient) { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + // When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission) + var handoverResources = new List(); + foreach (Identifier identifier in resourceClusters.Keys) + { + if (relevantLevelResources.TryGetValue(identifier, out var availableResources)) + { + var collectedResources = availableResources.Where(HasBeenCollected); + if (!collectedResources.Any()) { continue; } + int handoverCount = (int)MathF.Round(resourceHandoverAmount * collectedResources.Count()); + for (int i = 0; i < handoverCount; i++) + { + handoverResources.Add(collectedResources.ElementAt(i)); + } + } + } + foreach (var resource in handoverResources) + { + resource.Remove(); + } } - GiveReward(); - completed = true; } foreach (var kvp in spawnedResources) { @@ -167,7 +227,6 @@ namespace Barotrauma spawnedResources.Clear(); relevantLevelResources.Clear(); missionClusterPositions.Clear(); - failed = !completed && state > 0; } private void FindRelevantLevelResources() @@ -237,5 +296,27 @@ namespace Barotrauma missionClusterPositions.Add((kvp.Key, pos)); } } + + protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true) + { + int i = 1; + foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + { + Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? ""); + Replace($"[resourcequantity{i}]", cluster.Amount.ToString()); + i++; + } + Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount)); + return message; + + void Replace(string find, string replace) + { + if (color) + { + replace = $"‖color:gui.orange‖{replace}‖end‖"; + } + message = message.Replace(find, replace); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 4975202c3..138b3b9f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -13,7 +13,8 @@ namespace Barotrauma abstract partial class Mission { public readonly MissionPrefab Prefab; - protected bool completed, failed; + private bool completed; + protected bool failed; protected Level level; @@ -36,12 +37,14 @@ namespace Barotrauma } } - protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + + private readonly CheckDataAction completeCheckDataAction; public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; - public LocalizedString Name => Prefab.Name; + public virtual LocalizedString Name => Prefab.Name; private readonly LocalizedString successMessage; public virtual LocalizedString SuccessMessage @@ -156,6 +159,12 @@ namespace Barotrauma Locations = locations; + var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); + if (endConditionElement != null) + { + completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); + } + for (int n = 0; n < 2; n++) { string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖"; @@ -276,6 +285,10 @@ namespace Barotrauma partial void ShowMessageProjSpecific(int missionState); + protected virtual LocalizedString ModifyMessage(LocalizedString message, bool color = true) + { + return message; + } private void TryTriggerEvents(int state) { @@ -330,19 +343,27 @@ namespace Barotrauma /// /// End the mission and give a reward if it was completed successfully /// - public virtual void End() + public void End() { - completed = true; + completed = + DetermineCompleted() && + (completeCheckDataAction == null ||completeCheckDataAction.GetSuccess()); if (Prefab.LocationTypeChangeOnCompleted != null) { ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); } GiveReward(); + EndMissionSpecific(completed); } - public void GiveReward() + protected abstract bool DetermineCompleted(); + + protected virtual void EndMissionSpecific(bool completed) { } + + + private void GiveReward() { - if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; } + if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; } int reward = GetReward(Submarine.MainSub); float baseExperienceGain = reward * 0.09f; @@ -511,7 +532,7 @@ namespace Barotrauma protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) { - var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.ServerAndClient) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobOrJobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); characterInfo.TeamID = teamType; if (positionToStayIn == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 1be2ee4d3..92f08baec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -146,14 +146,24 @@ namespace Barotrauma tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); - Name = - TextManager.Get($"MissionName.{TextIdentifier}") - .Fallback(TextManager.Get(element.GetAttributeString("name", ""))) - .Fallback(element.GetAttributeString("name", "")); - Description = - TextManager.Get($"MissionDescription.{TextIdentifier}") - .Fallback(TextManager.Get(element.GetAttributeString("description", ""))) - .Fallback(element.GetAttributeString("description", "")); + string nameTag = element.GetAttributeString("name", ""); + Name = TextManager.Get($"MissionName.{TextIdentifier}"); + if (!string.IsNullOrEmpty(nameTag)) + { + Name = Name + .Fallback(TextManager.Get(nameTag)) + .Fallback(nameTag); + } + + string descriptionTag = element.GetAttributeString("description", ""); + Description = + TextManager.Get($"MissionDescription.{TextIdentifier}"); + if (!string.IsNullOrEmpty(descriptionTag)) + { + Description = Description + .Fallback(TextManager.Get(descriptionTag)) + .Fallback(descriptionTag); + } Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); @@ -167,23 +177,35 @@ namespace Barotrauma Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - SuccessMessage = - TextManager.Get($"MissionSuccess.{TextIdentifier}") - .Fallback(TextManager.Get(element.GetAttributeString("successmessage", ""))) - .Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); - FailureMessage = - TextManager.Get($"MissionFailure.{TextIdentifier}") - .Fallback(TextManager.Get(element.GetAttributeString("missionfailed", ""))) - .Fallback(TextManager.Get("missionfailed")) - .Fallback(GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); + string successMessageTag = element.GetAttributeString("successmessage", ""); + SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); + if (!string.IsNullOrEmpty(successMessageTag)) + { + SuccessMessage = SuccessMessage + .Fallback(TextManager.Get(successMessageTag)) + .Fallback(successMessageTag); + } + SuccessMessage = SuccessMessage.Fallback(TextManager.Get("missioncompleted")); + + string failureMessageTag = element.GetAttributeString("failuremessage", ""); + FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}"); + if (!string.IsNullOrEmpty(failureMessageTag)) + { + FailureMessage = FailureMessage + .Fallback(TextManager.Get(failureMessageTag)) + .Fallback(failureMessageTag); + } + FailureMessage = FailureMessage.Fallback(TextManager.Get("missionfailed")); string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); - - SonarLabel = + SonarLabel = TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") .Fallback(TextManager.Get(sonarLabelTag)) - .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}")) - .Fallback(element.GetAttributeString("sonarlabel", "")); + .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}")); + if (!string.IsNullOrEmpty(sonarLabelTag)) + { + SonarLabel = SonarLabel.Fallback(sonarLabelTag); + } SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index f861c2d10..9b3641502 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -2,21 +2,19 @@ using System.Collections.Generic; using System.Linq; using System; +using Barotrauma.Extensions; namespace Barotrauma { partial class MonsterMission : Mission { - //string = filename, point = min,max private readonly HashSet<(CharacterPrefab character, Point amountRange)> monsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>(); private readonly List monsters = new List(); private readonly List sonarPositions = new List(); - private readonly List tempSonarPositions = new List(); - private readonly float maxSonarMarkerDistance = 10000.0f; - private readonly Level.PositionType spawnPosType; + private Vector2? spawnPos = null; public override IEnumerable SonarPositions { @@ -114,7 +112,16 @@ namespace Barotrauma if (!IsClient) { - Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); + float minDistBetweenMonsterMissions = 10000; + float mindDistFromSub = Level.Loaded.Size.X * 0.3f; + var monsterMissions = GameMain.GameSession.Missions.Select(e => e as MonsterMission).Where(m => m != null && m != this && m.spawnPos.HasValue); + if (!Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out Vector2 spawnPos, + filter: p => monsterMissions.None(m => Vector2.DistanceSquared(p.Position.ToVector2(), m.spawnPos.Value) < minDistBetweenMonsterMissions * minDistBetweenMonsterMissions), + suppressWarning: true)) + { + Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out spawnPos); + } + this.spawnPos = spawnPos; foreach (var (character, amountRange) in monsterPrefabs) { int amount = Rand.Range(amountRange.X, amountRange.Y + 1); @@ -123,9 +130,8 @@ namespace Barotrauma monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false)); } } - InitializeMonsters(monsters); - } + } } private void InitializeMonsters(IEnumerable monsters) @@ -181,7 +187,7 @@ namespace Barotrauma } if (monsters[i].Removed || monsters[i].IsDead) { continue; } - Vector2 diff = tempSonarPositions[i] - monsters[i].Position; + Vector2 diff = tempSonarPositions[i] - monsters[i].WorldPosition; float maxDist = maxSonarMarkerDistance; Submarine refSub = Character.Controlled?.Submarine ?? Submarine.MainSub; @@ -191,12 +197,12 @@ namespace Barotrauma float subDist = Vector2.Distance(refPos, tempSonarPositions[i]) / maxDist; maxDist = Math.Min(subDist * subDist * maxDist, maxDist); - maxDist = Math.Min(Vector2.Distance(refPos, monsters[i].Position), maxDist); + maxDist = Math.Min(Vector2.Distance(refPos, monsters[i].WorldPosition), maxDist); } if (diff.LengthSquared() > maxDist * maxDist) { - tempSonarPositions[i] = monsters[i].Position + Vector2.Normalize(diff) * maxDist; + tempSonarPositions[i] = monsters[i].WorldPosition + Vector2.Normalize(diff) * maxDist; } } @@ -217,26 +223,26 @@ namespace Barotrauma break; } } - - public override void End() + + protected override bool DetermineCompleted() + { + return state > 0; + } + + protected override void EndMissionSpecific(bool completed) { tempSonarPositions.Clear(); monsters.Clear(); - if (State < 1) { return; } - - if (Prefab.LocationTypeChangeOnCompleted != null) + if (completed) { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } - GiveReward(); - completed = true; - if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) - { - level.LevelData.HasHuntingGrounds = false; + if (level?.LevelData != null && Prefab.Tags.Contains("huntinggrounds")) + { + level.LevelData.HasHuntingGrounds = false; + } } } - public bool IsEliminated(Character enemy) => + public static bool IsEliminated(Character enemy) => enemy == null || enemy.Removed || enemy.IsDead || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 5a85ee491..bc71d49dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -305,20 +305,13 @@ namespace Barotrauma return true; } - public override void End() + protected override bool DetermineCompleted() + { + return AllItemsDestroyedOrRetrieved(); + } + + protected override void EndMissionSpecific(bool completed) { - if (AllItemsDestroyedOrRetrieved()) - { - GiveReward(); - completed = true; - if (completed) - { - if (Prefab.LocationTypeChangeOnCompleted != null) - { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } - } - } foreach (Item item in items) { if (item != null && !item.Removed) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 20d26fbf5..ff33e7ec9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -401,13 +401,13 @@ namespace Barotrauma return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } - public override void End() + protected override bool DetermineCompleted() + { + return state == 2; + } + + protected override void EndMissionSpecific(bool completed) { - if (state == 2) - { - GiveReward(); - completed = true; - } characters.Clear(); characterItems.Clear(); failed = !completed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 858e8a74d..0d1b41b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -242,7 +242,7 @@ namespace Barotrauma Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; if (parentSub == null || parentSub.Info.Type != SubmarineType.Player) { - return; + return; } } State = 1; @@ -254,23 +254,16 @@ namespace Barotrauma } } - public override void End() + protected override bool DetermineCompleted() { var root = item?.GetRootContainer() ?? item; - if (root?.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndExit && !root.CurrentHull.Submarine.AtStartExit) || item.Removed) - { - return; - } - - if (Prefab.LocationTypeChangeOnCompleted != null) - { - ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); - } + return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed; + } + protected override void EndMissionSpecific(bool completed) + { item?.Remove(); item = null; - GiveReward(); - completed = true; failed = !completed && state > 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index 132767396..a59c69f8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -247,24 +247,9 @@ namespace Barotrauma } } - public override void End() + protected override bool DetermineCompleted() { - if (State == 2 && AllScannersReturned()) - { - GiveReward(); - completed = true; - } - foreach (var scanner in scanners) - { - if (scanner.Item != null && !scanner.Item.Removed) - { - scanner.OnScanStarted -= OnScanStarted; - scanner.OnScanCompleted -= OnScanCompleted; - scanner.Item.Remove(); - } - } - Reset(); - failed = !completed && state > 0; + return State == 2 && AllScannersReturned(); bool AllScannersReturned() { @@ -285,5 +270,20 @@ namespace Barotrauma return true; } } + + protected override void EndMissionSpecific(bool completed) + { + foreach (var scanner in scanners) + { + if (scanner.Item != null && !scanner.Item.Removed) + { + scanner.OnScanStarted -= OnScanStarted; + scanner.OnScanCompleted -= OnScanCompleted; + scanner.Item.Remove(); + } + } + Reset(); + failed = !completed && state > 0; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index c26b3f54b..1bdbade40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public readonly Identifier SpeciesName; public readonly int MinAmount, MaxAmount; - private List monsters; + private readonly List monsters = new List(); private readonly float scatter; private readonly float offset; @@ -30,7 +30,7 @@ namespace Barotrauma public readonly int MaxAmountPerLevel = int.MaxValue; - public List Monsters => monsters; + public IReadOnlyList Monsters => monsters; public Vector2? SpawnPos => spawnPos; public bool SpawnPending => spawnPending; @@ -115,7 +115,7 @@ namespace Barotrauma } } - private Submarine GetReferenceSub() + private static Submarine GetReferenceSub() { return EventManager.GetRefEntity() as Submarine ?? Submarine.MainSub; } @@ -146,6 +146,32 @@ namespace Barotrauma { DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); } + + monsters.Clear(); + + //+1 because Range returns an integer less than the max value + int amount = Rand.Range(MinAmount, MaxAmount + 1); + for (int i = 0; i < amount; i++) + { + string seed = Level.Loaded.Seed + i.ToString(); + Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); + if (createdCharacter == null) + { + DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\"."); + disallowed = true; + continue; + } + if (GameMain.GameSession.IsCurrentLocationRadiated()) + { + AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; + Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength); + createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction); + // TODO test multiplayer + createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false); + } + createdCharacter.DisabledByEvent = true; + monsters.Add(createdCharacter); + } } private List GetAvailableSpawnPositions() @@ -486,9 +512,6 @@ namespace Barotrauma spawnPending = false; - //+1 because Range returns an integer less than the max value - int amount = Rand.Range(MinAmount, MaxAmount + 1); - monsters = new List(); float scatterAmount = scatter; if (SpawnPosType.HasFlag(Level.PositionType.SidePath)) { @@ -507,9 +530,9 @@ namespace Barotrauma scatterAmount = 0; } - for (int i = 0; i < amount; i++) + int i = 0; + foreach (Character monster in monsters) { - string seed = Level.Loaded.Seed + i.ToString(); CoroutineManager.Invoke(() => { //round ended before the coroutine finished @@ -532,45 +555,33 @@ namespace Barotrauma } } - Character createdCharacter = Character.Create(SpeciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); - if (createdCharacter == null) - { - disallowed = true; - return; - } - + monster.Enabled = true; + monster.DisabledByEvent = false; + monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos)); + var eventManager = GameMain.GameSession.EventManager; if (eventManager != null) { if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath)) { - eventManager.CumulativeMonsterStrengthMain += createdCharacter.Params.AI.CombatStrength; + eventManager.CumulativeMonsterStrengthMain += monster.Params.AI.CombatStrength; eventManager.AddTimeStamp(this); } else if (SpawnPosType.HasFlag(Level.PositionType.Ruin)) { - eventManager.CumulativeMonsterStrengthRuins += createdCharacter.Params.AI.CombatStrength; + eventManager.CumulativeMonsterStrengthRuins += monster.Params.AI.CombatStrength; } else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) { - eventManager.CumulativeMonsterStrengthWrecks += createdCharacter.Params.AI.CombatStrength; + eventManager.CumulativeMonsterStrengthWrecks += monster.Params.AI.CombatStrength; } else if (SpawnPosType.HasFlag(Level.PositionType.Cave)) { - eventManager.CumulativeMonsterStrengthCaves += createdCharacter.Params.AI.CombatStrength; + eventManager.CumulativeMonsterStrengthCaves += monster.Params.AI.CombatStrength; } } - if (GameMain.GameSession.IsCurrentLocationRadiated()) - { - AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; - Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength); - createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction); - // TODO test multiplayer - createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false); - } - monsters.Add(createdCharacter); - if (monsters.Count == amount) + if (monster == monsters.Last()) { spawnReady = true; //this will do nothing if the monsters have no swarm behavior defined, @@ -586,6 +597,7 @@ namespace Barotrauma value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); } }, delayBetweenSpawns * i); + i++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 4039e31dc..9ea8ff2a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -298,5 +298,15 @@ namespace Barotrauma.Extensions return null; } + + public static IEnumerable NotNull(this IEnumerable source) where T : struct + => source + .Where(nullable => nullable.HasValue) + .Select(nullable => nullable.Value); + + public static IEnumerable NotNone(this IEnumerable> source) + => source + .OfType>() + .Select(some => some.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index b254682e4..3b56fd1aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -1,20 +1,21 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { - public static class StringExtensions + static class StringExtensions { public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s; - public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s); - public static bool IsNullOrWhiteSpace(this string? s) => string.IsNullOrWhiteSpace(s); - public static bool IsNullOrEmpty(this ContentPath? p) => p?.IsNullOrEmpty() ?? true; - public static bool IsNullOrWhiteSpace(this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; - public static bool IsNullOrEmpty(this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); - public static bool IsNullOrWhiteSpace(this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); - public static bool IsNullOrEmpty(this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); - public static bool IsNullOrWhiteSpace(this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index ac9e379a4..a122a213c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -342,6 +342,21 @@ namespace Barotrauma } private static Implementation? loadedImplementation; + private static void ValidateEventID(string eventID) + { +#if DEBUG + string[] parts = eventID.Split(':'); + if (parts.Length > 5) + { + DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Only 5 id parts allowed separated by ':'"); + } + if (parts.Any(p => p.Length > 32)) + { + DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Each id part separated by ':' must be 32 characters or less."); + } +#endif + } + public static void AddErrorEvent(ErrorSeverity errorSeverity, string message) { if (!SendUserStatistics) { return; } @@ -368,12 +383,14 @@ namespace Barotrauma public static void AddDesignEvent(string eventID) { if (!SendUserStatistics) { return; } + ValidateEventID(eventID); loadedImplementation?.AddDesignEvent(eventID); } public static void AddDesignEvent(string eventID, double value) { if (!SendUserStatistics) { return; } + ValidateEventID(eventID); loadedImplementation?.AddDesignEvent(eventID, value); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index c4d5be01a..6fa21d9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -80,7 +80,7 @@ namespace Barotrauma { if (ID != Entity.NullEntityID) { - DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed."); + DebugConsole.LogError("Error setting SoldItem.ID: ID has already been set and should not be changed."); return; } ID = id; @@ -128,7 +128,7 @@ namespace Barotrauma { if (Item != null) { - DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}"); return; } Item = item; @@ -302,6 +302,10 @@ namespace Barotrauma var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true); foreach (PurchasedItem item in itemsToPurchase) { + // Exchange money + int itemValue = item.Quantity * buyValues[item.ItemPrefab]; + if (!campaign.TryPurchase(client, itemValue)) { continue; } + // Add to the purchased items var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab); if (purchasedItem != null) @@ -313,9 +317,6 @@ namespace Barotrauma purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); itemsPurchasedFromStore.Add(purchasedItem); } - // Exchange money - int itemValue = item.Quantity * buyValues[item.ItemPrefab]; - campaign.TryPurchase(client, itemValue); if (GameMain.IsSingleplayer) { GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 421af50e0..06ab5566e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -1,4 +1,7 @@ using Barotrauma.Extensions; +#if CLIENT +using Barotrauma.Tutorials; +#endif using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -195,6 +198,34 @@ namespace Barotrauma } } + /// + /// Remove the character from the crew (and crew menus). + /// + /// The character to remove + /// If the character info is also removed, the character will not be visible in the round summary. + public void RemoveCharacter(Character character, bool removeInfo = false, bool resetCrewListIndex = true) + { + if (character == null) + { + DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } + characters.Remove(character); + if (removeInfo) + { + characterInfos.Remove(character.Info); +#if CLIENT + RemoveCharacterFromCrewList(character); +#endif + } +#if CLIENT + if (resetCrewListIndex) + { + ResetCrewListIndex(character); + } +#endif + } + public void AddCharacterInfo(CharacterInfo characterInfo) { if (characterInfos.Contains(characterInfo)) @@ -348,6 +379,9 @@ namespace Barotrauma private void UpdateConversations(float deltaTime) { if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial is Tutorial tutorial && tutorial.TutorialPrefab.DisableBotConversations) { return; } +#endif if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; } conversationTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 1d88c39a6..06dc62593 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -96,20 +96,17 @@ namespace Barotrauma private object GetTypeOrDefault(Identifier identifier, Type type, object defaultValue) { object? value = GetValue(identifier); - - if (value == null) + if (value != null) { - SetValue(identifier, defaultValue); + if (value.GetType() == type) + { + return value; + } + else + { + DebugConsole.ThrowError($"Attempted to get value \"{identifier}\" as a {type} but the value is {value.GetType()}."); + } } - else if (value.GetType() == type) - { - return value; - } - else - { - DebugConsole.ThrowError($"Attempted to get value \"{identifier}\" as a {type} but the value is {value.GetType()}."); - } - return defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 7bf83c372..b6868a586 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -34,7 +34,7 @@ namespace Barotrauma public double TotalPlayTime; public int TotalPassedLevels; - public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic, Cargo } + public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Upgrade, PurchaseSub, MedicalClinic, Cargo } public static bool BlocksInteraction(InteractionType interactionType) { @@ -85,7 +85,9 @@ namespace Barotrauma public bool CheatsEnabled; - public const int HullRepairCost = 500, ItemRepairCost = 500, ShuttleReplaceCost = 1000; + public const float HullRepairCostPerDamage = 0.5f, ItemRepairCostPerRepairDuration = 1.0f; + public const int ShuttleReplaceCost = 1000; + public const int MaxHullRepairCost = 2000, MaxItemRepairCost = 2000; protected bool wasDocked; @@ -131,6 +133,8 @@ namespace Barotrauma protected set; } + public bool PurchasedLostShuttlesInLatestSave, PurchasedHullRepairsInLatestSave, PurchasedItemRepairsInLatestSave; + public virtual bool PurchasedHullRepairs { get; set; } public virtual bool PurchasedLostShuttles { get; set; } public virtual bool PurchasedItemRepairs { get; set; } @@ -152,8 +156,9 @@ namespace Barotrauma { if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } - bool isGain = changed > 0; + if (changed != 0) { return; } + bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; switch (e.Owner) @@ -225,7 +230,8 @@ namespace Barotrauma #if CLIENT prevCampaignUIAutoOpenType = TransitionType.None; #endif - if (PurchasedHullRepairs) + + if (PurchasedHullRepairsInLatestSave) { foreach (Structure wall in Structure.WallList) { @@ -238,9 +244,9 @@ namespace Barotrauma } } } - PurchasedHullRepairs = false; + PurchasedHullRepairsInLatestSave = PurchasedHullRepairs = false; } - if (PurchasedItemRepairs) + if (PurchasedItemRepairsInLatestSave) { foreach (Item item in Item.ItemList) { @@ -253,13 +259,46 @@ namespace Barotrauma } } } - PurchasedItemRepairs = false; + PurchasedItemRepairsInLatestSave = PurchasedItemRepairs = false; } - PurchasedLostShuttles = false; + PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); } + public int GetHullRepairCost() + { + float totalDamage = 0; + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine)) + { + for (int i = 0; i < wall.SectionCount; i++) + { + totalDamage += wall.SectionDamage(i); + } + } + } + return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost); + } + + public int GetItemRepairCost() + { + float totalRepairDuration = 0.0f; + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine)) + { + var repairable = item.GetComponent(); + if (repairable == null) { continue; } + totalRepairDuration += repairable.FixDurationHighSkill * (1.0f - item.Condition / item.MaxCondition); + } + } + return (int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost); + } + public void InitCampaignData() { Factions = new List(); @@ -689,7 +728,7 @@ namespace Barotrauma } foreach (LocationConnection connection in Map.Connections) { - connection.Difficulty = connection.Biome.MaxDifficulty; + connection.Difficulty = connection.Biome.AdjustedMaxDifficulty; connection.LevelData = new LevelData(connection) { IsBeaconActive = false @@ -698,7 +737,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, location.Biome.MaxDifficulty); + location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs index cc4948562..5c260e037 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,27 +1,16 @@ using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { partial class CharacterCampaignData { - public CharacterInfo CharacterInfo - { - get; - private set; - } + public readonly CharacterInfo CharacterInfo; public readonly string Name; - public string ClientEndPoint - { - get; - private set; - } - public ulong SteamID - { - get; - private set; - } + public readonly Address ClientAddress; + public readonly Option AccountId; private XElement itemData; private XElement healthData; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 18a0f6fcf..9e1e19aa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -10,8 +10,6 @@ namespace Barotrauma { partial class MultiPlayerCampaign : CampaignMode { - public const int MinimumInitialMoney = 500; - [Flags] public enum NetFlags : UInt16 { @@ -151,9 +149,9 @@ namespace Barotrauma /// private void Load(XElement element) { - PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); - PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); - PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); + PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false); + PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false); + PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false); CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); if (CheatsEnabled) { @@ -294,14 +292,14 @@ namespace Barotrauma private static void WriteItems(IWriteMessage msg, Dictionary> purchasedItems) { - msg.Write((byte)purchasedItems.Count); + msg.WriteByte((byte)purchasedItems.Count); foreach (var storeItems in purchasedItems) { - msg.Write(storeItems.Key); - msg.Write((UInt16)storeItems.Value.Count); + msg.WriteIdentifier(storeItems.Key); + msg.WriteUInt16((UInt16)storeItems.Value.Count); foreach (var item in storeItems.Value) { - msg.Write(item.ItemPrefabIdentifier); + msg.WriteIdentifier(item.ItemPrefabIdentifier); msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity); } } @@ -328,18 +326,18 @@ namespace Barotrauma private static void WriteItems(IWriteMessage msg, Dictionary> soldItems) { - msg.Write((byte)soldItems.Count); + msg.WriteByte((byte)soldItems.Count); foreach (var storeItems in soldItems) { - msg.Write(storeItems.Key); - msg.Write((UInt16)storeItems.Value.Count); + msg.WriteIdentifier(storeItems.Key); + msg.WriteUInt16((UInt16)storeItems.Value.Count); foreach (var item in storeItems.Value) { - msg.Write(item.ItemPrefab.Identifier); - msg.Write((UInt16)item.ID); - msg.Write(item.Removed); - msg.Write(item.SellerID); - msg.Write((byte)item.Origin); + msg.WriteIdentifier(item.ItemPrefab.Identifier); + msg.WriteUInt16((UInt16)item.ID); + msg.WriteBoolean(item.Removed); + msg.WriteByte(item.SellerID); + msg.WriteByte((byte)item.Origin); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs index 1ac387dee..4599c3857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs @@ -1,5 +1,7 @@ +using Barotrauma.Extensions; using Barotrauma.Networking; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -11,37 +13,37 @@ namespace Barotrauma public void AssignTeamIDs(IEnumerable clients) { - int teamWeight = 0; - List randList = new List(clients); - for (int i = 0; i < randList.Count; i++) + int team1Count = 0, team2Count = 0; + //if a client has a preference, assign them to that team + List unassignedClients = new List(clients); + for (int i = 0; i < unassignedClients.Count; i++) { - if (randList[i].PreferredTeam == CharacterTeamType.Team1 || - randList[i].PreferredTeam == CharacterTeamType.Team2) + if (unassignedClients[i].PreferredTeam == CharacterTeamType.Team1 || + unassignedClients[i].PreferredTeam == CharacterTeamType.Team2) { - randList[i].TeamID = randList[i].PreferredTeam; - teamWeight += randList[i].PreferredTeam == CharacterTeamType.Team1 ? -1 : 1; - randList.RemoveAt(i); + assignTeam(unassignedClients[i], unassignedClients[i].PreferredTeam); i--; } } - for (int i = 0; i Prefabs = +#if CLIENT + new PrefabCollection(onSort: MainMenuScreen.UpdateInstanceTutorialButtons); +#else + new PrefabCollection(); +#endif + + public readonly int Order; + public readonly bool DisableBotConversations; + public readonly bool AllowCharacterSwitch; + + public readonly ContentPath SubmarinePath = ContentPath.FromRaw("Content/Tutorials/Dugong_Tutorial.sub"); + public readonly ContentPath OutpostPath = ContentPath.FromRaw("Content/Tutorials/TutorialOutpost.sub"); + public readonly string LevelSeed; + public readonly string LevelParams; + + private readonly ContentXElement tutorialCharacterElement; + public readonly ImmutableArray StartingItemTags; + + public readonly Identifier EventIdentifier; + + public readonly Sprite Banner; + + public TutorialPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Order = element.GetAttributeInt("order", int.MaxValue); + DisableBotConversations = element.GetAttributeBool("disablebotconversations", true); + AllowCharacterSwitch = element.GetAttributeBool("allowcharacterswitch", false); + + SubmarinePath = element.GetAttributeContentPath("submarinepath") ?? SubmarinePath; + OutpostPath = element.GetAttributeContentPath("outpostpath") ?? OutpostPath; + LevelSeed = element.GetAttributeString("levelseed", "nLoZLLtza"); + LevelParams = element.GetAttributeString("levelparams", "ColdCavernsTutorial"); + + tutorialCharacterElement = element.GetChildElement("characterinfo"); + if (tutorialCharacterElement != null) + { + StartingItemTags = tutorialCharacterElement + .GetAttributeIdentifierArray("startingitemtags", new Identifier[0]) + .ToImmutableArray(); + } + else + { + StartingItemTags = ImmutableArray.Empty; + } + + var bannerElement = element.GetChildElement("banner"); + if (bannerElement != null) + { + Banner = new Sprite(bannerElement, lazyLoad: true); + } + + EventIdentifier = element.GetChildElement("scriptedevent")?.GetAttributeIdentifier("identifier", "") ?? Identifier.Empty; + } + + public CharacterInfo GetTutorialCharacterInfo() + { + if (tutorialCharacterElement == null) + { + return null; + } + Identifier speciesName = tutorialCharacterElement.GetAttributeIdentifier("speciesname", CharacterPrefab.HumanSpeciesName); + Identifier jobPrefabIdentifier = tutorialCharacterElement.GetAttributeIdentifier("jobidentifier", "assistant"); + if (!JobPrefab.Prefabs.TryGet(jobPrefabIdentifier, out var jobPrefab)) + { + jobPrefab = JobPrefab.Prefabs.First(); + } + int jobVariant = tutorialCharacterElement.GetAttributeInt("variant", 0); + var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: jobPrefab, variant: jobVariant); + foreach (var skillElement in tutorialCharacterElement.GetChildElements("skill")) + { + Identifier skillIdentifier = skillElement.GetAttributeIdentifier("identifier", ""); + if (skillIdentifier.IsEmpty) { continue; } + float level = skillElement.GetAttributeFloat("level", 0.0f); + characterInfo.SetSkillLevel(skillIdentifier, level); + } + return characterInfo; + } + + public override void Dispose() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 0e238a4ef..46cdadcf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -211,7 +211,7 @@ namespace Barotrauma if (selectedSub != null) { campaign.Bank.Deduct(selectedSub.Price); - campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0); } return campaign; } @@ -222,7 +222,7 @@ namespace Barotrauma if (selectedSub != null) { campaign.Bank.TryDeduct(selectedSub.Price); - campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0); } return campaign; } @@ -604,7 +604,7 @@ namespace Barotrauma if (GameMode is MultiPlayerCampaign mpCampaign) { mpCampaign.UpgradeManager.ApplyUpgrades(); - mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine); + mpCampaign.UpgradeManager.SanityCheckUpgrades(); } } @@ -874,7 +874,7 @@ namespace Barotrauma double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, - GameMode?.Name?.Value ?? "none", + GameMode?.Preset.Identifier.Value ?? "none", roundDuration); string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; LogEndRoundStats(eventId); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index b133e0228..a35b86654 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -60,16 +60,6 @@ namespace Barotrauma /// public const bool UpgradeAlsoConnectedSubs = true; - /// - /// Prevents the player from upgrading the submarine when we are switching to a new one. - /// - /// - /// In singleplayer we check if CampaignMode.PendingSubmarineSwitch is not null indicating we are switching submarines - /// but in multiplayer that value is not synced so we use this variable instead by setting it to false in - /// and then set it back to true when the round ends in - /// - public bool CanUpgrade = true; - /// /// This is used by the client in multiplayer, acts like a secondary PendingUpgrades list /// but is not affected by server messages. @@ -422,14 +412,14 @@ namespace Barotrauma #endif } - public List GetLinkedItemsToSwap(Item item) + public static ICollection GetLinkedItemsToSwap(Item item) { - List linkedItems = new List() { item }; + HashSet linkedItems = new HashSet() { item }; foreach (MapEntity linkedEntity in item.linkedTo) { foreach (MapEntity secondLinkedEntity in linkedEntity.linkedTo) { - if (!(secondLinkedEntity is Item linkedItem) || linkedItem == item) { continue; } + if (secondLinkedEntity is not Item linkedItem || linkedItem == item) { continue; } if (linkedItem.AllowSwapping && linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == ((MapEntity)linkedItem).Prefab.Identifier) && linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)) @@ -522,8 +512,11 @@ namespace Barotrauma /// Should be called after every round start right after /// /// - public void SanityCheckUpgrades(Submarine submarine) + public void SanityCheckUpgrades() { + Submarine submarine = GameMain.GameSession?.Submarine ?? Submarine.MainSub; + if (submarine is null) { return; } + // check walls foreach (Structure wall in submarine.GetWalls(UpgradeAlsoConnectedSubs)) { @@ -531,23 +524,8 @@ namespace Barotrauma { foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) { - int level = GetRealUpgradeLevel(prefab, category); - if (level == 0 || !prefab.IsWallUpgrade) { continue; } - - Upgrade? upgrade = wall.GetUpgrade(prefab.Identifier); - - bool isOverMax = IsOverMaxLevel(level, prefab); - if (isOverMax) - { - SetUpgradeLevel(prefab, category, prefab.MaxLevel); - level = prefab.MaxLevel; - } - - if (upgrade == null || upgrade.Level != level || isOverMax) - { - DebugLog($"{((MapEntity)wall).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); - FixUpgradeOnItem(wall, prefab, level); - } + if (!prefab.IsWallUpgrade) { continue; } + TryFixUpgrade(wall, category, prefab); } } } @@ -559,35 +537,38 @@ namespace Barotrauma { foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) { - if (!category.CanBeApplied(item, prefab)) { continue; } - - int level = GetRealUpgradeLevel(prefab, category); - if (level == 0) { continue; } - - Upgrade? upgrade = item.GetUpgrade(prefab.Identifier); - bool isOverMax = IsOverMaxLevel(level, prefab); - if (isOverMax) - { - SetUpgradeLevel(prefab, category, prefab.MaxLevel); - level = prefab.MaxLevel; - } - - if (upgrade == null || upgrade.Level != level || isOverMax) - { - DebugLog($"{((MapEntity)item).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); - FixUpgradeOnItem(item, prefab, level); - } + TryFixUpgrade(item, category, prefab); } } } - static bool IsOverMaxLevel(int level, UpgradePrefab prefab) => level > prefab.MaxLevel; + void TryFixUpgrade(MapEntity entity, UpgradeCategory category, UpgradePrefab prefab) + { + if (!category.CanBeApplied(entity, prefab)) { return; } + + int level = GetRealUpgradeLevel(prefab, category); + int maxLevel = submarine.Info is { } info ? prefab.GetMaxLevel(info) : prefab.MaxLevel; + if (maxLevel < level) { level = maxLevel; } + + if (level == 0) { return; } + + Upgrade? upgrade = entity.GetUpgrade(prefab.Identifier); + + if (upgrade == null || upgrade.Level != level) + { + DebugLog($"{entity.Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); + FixUpgradeOnItem((ISerializableEntity)entity, prefab, level); + } + } } private static void FixUpgradeOnItem(ISerializableEntity target, UpgradePrefab prefab, int level) { if (target is MapEntity mapEntity) { + // do not fix what's not broken + if (level == 0) { return; } + mapEntity.SetUpgrade(new Upgrade(target, prefab, level), false); } } @@ -713,9 +694,9 @@ namespace Barotrauma public bool CanUpgradeSub() { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return CanUpgrade; } - - return Campaign.PendingSubmarineSwitch == null; + return + Campaign.PendingSubmarineSwitch == null || + Campaign.PendingSubmarineSwitch.Name == Submarine.MainSub.Info.Name; } public void Save(XElement? parent) @@ -728,7 +709,7 @@ namespace Barotrauma SavePendingUpgrades(upgradeManagerElement, PendingUpgrades); } - private void SavePendingUpgrades(XElement? parent, List upgrades) + private static void SavePendingUpgrades(XElement? parent, List upgrades) { if (parent == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 2b496a231..911fc6a4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -12,7 +12,7 @@ namespace Barotrauma Ragdoll, Health, Grab, SelectNextCharacter, SelectPreviousCharacter, - Voice, + Voice, RadioVoice, LocalVoice, Deselect, Shoot, Command, @@ -23,5 +23,6 @@ namespace Barotrauma PreviousFireMode, ActiveChat, ToggleChatMode, + ChatBox } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 405b6a22a..8ef6c05ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components private float forceLockTimer; //if the submarine isn't in the correct position to lock within this time after docking has been activated, //force the sub to the correct position - const float ForceLockDelay = 1.0f; + const float ForceLockDelay = 1.0f; public int DockingDir { get; set; } @@ -81,12 +81,18 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(DirectionType.None, IsPropertySaveable.No, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ + [Editable, Serialize(DirectionType.None, IsPropertySaveable.No, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n" + "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } - + public DockingPort DockingTarget { get; private set; } + /// + /// Can be used by status effects + /// + public bool AtStartExit => Item.Submarine is { AtStartExit: true}; + public bool AtEndExit => Item.Submarine is { AtEndExit: true }; + public Door Door { get; private set; } public bool Docked @@ -116,6 +122,8 @@ namespace Barotrauma.Items.Components get { return joint is WeldJoint || DockingTarget?.joint is WeldJoint; } } + public bool AnotherPortInProximity => FindAdjacentPort() != null; + /// /// Automatically cleared after docking -> no need to unregister /// @@ -989,7 +997,7 @@ namespace Barotrauma.Items.Components dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f); if (dockingState < 0.01f) { docked = false; } item.SendSignal("0", "state_out"); - item.SendSignal((FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor"); + item.SendSignal(AnotherPortInProximity ? "1" : "0", "proximity_sensor"); } else { @@ -1191,7 +1199,7 @@ namespace Barotrauma.Items.Components //trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character // -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port // in a way that makes always makes it dock/undock immediately at the start of the roun - if (tryingToToggleOutpostDocking && signal.sender == null) + if (GameMain.NetworkMember != null && tryingToToggleOutpostDocking && signal.sender == null) { if (allowOutpostAutoDocking == AllowOutpostAutoDocking.Ask) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 76dd68595..8188d52c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components { if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption) { - List batteries = GetConnectedBatteries(); + List batteries = GetDirectlyConnectedBatteries(); float neededPower = PowerConsumption; while (neededPower > 0.0001f && batteries.Count > 0) { @@ -203,7 +203,7 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(null, node.WorldPosition, attack, 1.0f); + character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); } } DischargeProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index bff4ee42c..ccb9ba004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -132,8 +132,8 @@ namespace Barotrauma.Items.Components public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null) { int flowerVariant = Growable.RandomInt(0, maxVariants, random); - float flowerScale = (float) Growable.RandomDouble(minScale, maxScale, random); - float flowerRotation = (float) Growable.RandomDouble(0, MathHelper.TwoPi, random); + float flowerScale = (float)Growable.RandomDouble(minScale, maxScale, random); + float flowerRotation = (float)Growable.RandomDouble(0, MathHelper.TwoPi, random); return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation }; } } @@ -169,10 +169,10 @@ namespace Barotrauma.Items.Components { const float limit = 1.0f; growthStep = value; - VineStep = Math.Min((float) Math.Pow(value, 2), limit); + VineStep = Math.Min((float)Math.Pow(value, 2), limit); if (value > limit) { - FlowerStep = Math.Min((float) Math.Pow(value - limit, 2), limit); + FlowerStep = Math.Min((float)Math.Pow(value - limit, 2), limit); } } } @@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components { if (Type == VineTileType.Stem) { return; } - Type = (VineTileType) Sides; + Type = (VineTileType)Sides; } /// @@ -310,7 +310,7 @@ namespace Barotrauma.Items.Components public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side); - public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size); + public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int)pos.X - Size / 2, (int)pos.Y + Size / 2, Size, Size); } internal static class GrowthSideExtension @@ -318,7 +318,7 @@ namespace Barotrauma.Items.Components // K&R algorithm for counting how many bits are set in a bit field public static int Count(this TileSide side) { - int n = (int) side; + int n = (int)side; int count = 0; while (n != 0) { @@ -607,7 +607,7 @@ namespace Barotrauma.Items.Components #if CLIENT foreach (VineTile vine in Vines) { - vine.DecayDelay = (float) RandomDouble(0f, 30f); + vine.DecayDelay = (float)RandomDouble(0f, 30f); } #endif #if SERVER @@ -742,7 +742,7 @@ namespace Barotrauma.Items.Components { var (x, y, z, w) = GrowthWeights; float[] weights = { x, y, z, w }; - int index = (int) Math.Log2((int) side); + int index = (int)Math.Log2((int)side); if (MathUtils.NearlyEqual(weights[index], 0f)) { oldVines.FailedGrowthAttempts++; @@ -778,7 +778,7 @@ namespace Barotrauma.Items.Components foreach (VineTile otherVine in Vines) { var (distX, distY) = pos - otherVine.Position; - int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY); + int absDistX = (int)Math.Abs(distX), absDistY = (int)Math.Abs(distY); // check if the tile is within the with or height distance from us but ignore diagonals if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; } @@ -872,10 +872,10 @@ namespace Barotrauma.Items.Components foreach (VineTile vine in Vines) { XElement vineElement = new XElement("Vine"); - vineElement.Add(new XAttribute("sides", (int) vine.Sides)); - vineElement.Add(new XAttribute("blockedsides", (int) vine.BlockedSides)); + vineElement.Add(new XAttribute("sides", (int)vine.Sides)); + vineElement.Add(new XAttribute("blockedsides", (int)vine.BlockedSides)); vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position))); - vineElement.Add(new XAttribute("tile", (int) vine.Type)); + vineElement.Add(new XAttribute("tile", (int)vine.Type)); vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts)); #if SERVER vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f)); @@ -902,10 +902,10 @@ namespace Barotrauma.Items.Components { if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase)) { - VineTileType type = (VineTileType) element.GetAttributeInt("tile", 0); + VineTileType type = (VineTileType)element.GetAttributeInt("tile", 0); Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); - TileSide sides = (TileSide) element.GetAttributeInt("sides", 0); - TileSide blockedSides = (TileSide) element.GetAttributeInt("blockedsides", 0); + TileSide sides = (TileSide)element.GetAttributeInt("sides", 0); + TileSide blockedSides = (TileSide)element.GetAttributeInt("blockedsides", 0); int failedAttempts = element.GetAttributeInt("failedattempts", 0); float growthscale = element.GetAttributeFloat("growthscale", 0f); int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 0fbda966d..5641b078f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -295,12 +295,11 @@ namespace Barotrauma.Items.Components if (attachable) { - DeattachFromWall(); - if (body != null) { item.body = body; } + DeattachFromWall(); } if (Pusher != null) { Pusher.Enabled = false; } @@ -619,6 +618,10 @@ namespace Barotrauma.Items.Components #if CLIENT item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth; #endif + foreach (LightComponent light in item.GetComponents()) + { + light.CheckIfNeedsUpdate(); + } } public override void ParseMsg() @@ -691,12 +694,30 @@ namespace Barotrauma.Items.Components { item.Drop(character); item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false); + //the light source won't get properly updated if lighting is disabled (even though the light sprite is still drawn when lighting is disabled) + //so let's ensure the light source is up-to-date + RefreshLightSources(item); } AttachToWall(); } return true; + + static void RefreshLightSources(Item item) + { + item.body?.UpdateDrawPosition(); + foreach (var light in item.GetComponents()) + { + light.SetLightSourceTransform(); + } + item.GetComponent()?.SetContainedItemPositions(); + foreach (var containedItem in item.ContainedItems) + { + RefreshLightSources(containedItem); + } + } } + public override bool SecondaryUse(float deltaTime, Character character = null) { return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 303271a7d..72e7d0a46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -1,12 +1,11 @@ -using Barotrauma.Networking; -using FarseerPhysics; +using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -64,7 +63,7 @@ namespace Barotrauma.Items.Components /// /// Defines items that boost the weapon functionality, like battery cell for stun batons. /// - public readonly Identifier[] PreferredContainedItems; + public readonly ImmutableHashSet PreferredContainedItems; public MeleeWeapon(Item item, ContentXElement element) : base(item, element) @@ -79,7 +78,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); - PreferredContainedItems = element.GetAttributeIdentifierArray("preferredcontaineditems", Array.Empty()); + PreferredContainedItems = element.GetAttributeIdentifierArray("preferredcontaineditems", Array.Empty()).ToImmutableHashSet(); } public override void Equip(Character character) @@ -292,7 +291,6 @@ namespace Barotrauma.Items.Components item.body.PhysEnabled = false; } - private bool OnCollision(Fixture f1, Fixture f2, Contact contact) { if (User == null || User.Removed) @@ -390,15 +388,17 @@ namespace Barotrauma.Items.Components User = null; return; } - + + float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); + damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); + Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target); if (Attack != null) { Attack.SetUser(User); - Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); - Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); + Attack.DamageMultiplier = damageMultiplier; if (targetLimb != null) { @@ -420,7 +420,18 @@ namespace Barotrauma.Items.Components else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } - Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); +#if CLIENT + if (attackResult.Damage > 0.0f) + { + Character.Controlled?.UpdateHUDProgressBar(targetItem, + targetItem.WorldPosition, + targetItem.Condition / targetItem.MaxCondition, + emptyColor: GUIStyle.HealthBarColorLow, + fullColor: GUIStyle.HealthBarColorHigh, + textTag: targetItem.Name); + } +#endif } else if (target.UserData is Holdable holdable && holdable.CanPush) { @@ -460,7 +471,7 @@ namespace Barotrauma.Items.Components if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User); + ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 5086b6899..95c40bb0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -165,31 +165,32 @@ namespace Barotrauma.Items.Components pickTimer = 0.0f; while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen) { - //cancel if the item is currently selected - //attempting to pick does not select the item, so if it is selected at this point, another ItemComponent - //must have been selected and we should not keep deattaching (happens when for example interacting with - //an electrical component while holding both a screwdriver and a wrench). - if (picker.SelectedConstruction == item || - picker.IsKeyDown(InputType.Aim) || - !picker.CanInteractWith(item) || - item.Removed || item.ParentInventory != null) + if (!CoroutineManager.Paused) { - StopPicking(picker); - yield return CoroutineStatus.Success; - } + //cancel if the item is currently selected + //attempting to pick does not select the item, so if it is selected at this point, another ItemComponent + //must have been selected and we should not keep deattaching (happens when for example interacting with + //an electrical component while holding both a screwdriver and a wrench). + if (picker.IsAnySelectedItem(item) || + picker.IsKeyDown(InputType.Aim) || + !picker.CanInteractWith(item) || + item.Removed || item.ParentInventory != null) + { + StopPicking(picker); + yield return CoroutineStatus.Success; + } #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - item.WorldPosition, - pickTimer / requiredTime, - GUIStyle.Red, GUIStyle.Green, - !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + Character.Controlled?.UpdateHUDProgressBar( + this, + item.WorldPosition, + pickTimer / requiredTime, + GUIStyle.Red, GUIStyle.Green, + !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); #endif - - picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); - pickTimer += CoroutineManager.DeltaTime; - + picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); + pickTimer += CoroutineManager.DeltaTime; + } yield return CoroutineStatus.Running; } @@ -208,7 +209,7 @@ namespace Barotrauma.Items.Components { if (picker != null) { - picker.AnimController.Anim = AnimController.Animation.None; + picker.AnimController.StopUsingItem(); picker.PickingItem = null; } if (pickingCoroutine != null) @@ -286,7 +287,7 @@ namespace Barotrauma.Items.Components public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(activePicker?.ID ?? (ushort)0); + msg.WriteUInt16(activePicker?.ID ?? (ushort)0); } public virtual void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index f7b360dad..9986d7130 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -18,6 +18,7 @@ namespace Barotrauma.Items.Components }; private readonly HashSet fixableEntities; + private readonly HashSet nonFixableEntities; private Vector2 pickedPosition; private float activeTimer; @@ -135,6 +136,7 @@ namespace Barotrauma.Items.Components } fixableEntities = new HashSet(); + nonFixableEntities = new HashSet(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -147,7 +149,16 @@ namespace Barotrauma.Items.Components } else { - fixableEntities.Add(subElement.GetAttributeIdentifier("identifier", "")); + foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty())) + { + fixableEntities.Add(id); + } + } + break; + case "nonfixable": + foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty())) + { + nonFixableEntities.Add(id); } break; } @@ -523,6 +534,7 @@ namespace Barotrauma.Items.Components if (sectionIndex < 0) { return false; } if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; } + if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 40d209d5b..df595ca57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -111,6 +111,12 @@ namespace Barotrauma.Items.Components } ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); + //return if the status effect got rid of the picker somehow + if (picker == null || picker.Removed || !picker.HeldItems.Contains(item)) + { + IsActive = false; + return; + } if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index a7fd18d5c..154734f0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -231,8 +231,6 @@ namespace Barotrauma.Items.Components set; } - public virtual bool RecreateGUIOnResolutionChange => false; - /// /// 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). /// @@ -399,7 +397,7 @@ namespace Barotrauma.Items.Components RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name); if (ri != null) { - if (ri.Identifiers.Length == 0) + if (ri.Identifiers.Count == 0) { DisabledRequiredItems.Add(ri); } @@ -818,7 +816,7 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f) + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f) { if (statusEffectLists == null) { return; } @@ -830,13 +828,14 @@ namespace Barotrauma.Items.Components { if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } + effect.AfflictionMultiplier = afflictionMultiplier; item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) { effect.AfflictionMultiplier = applyOnUserFraction; item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); - effect.AfflictionMultiplier = 1.0f; } + effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } //if any of the effects reduce the item's condition, set the user for OnBroken effects as well @@ -1072,7 +1071,7 @@ namespace Barotrauma.Items.Components AIObjectiveContainItem containObjective = null; if (character.AIController is HumanAIController aiController) { - containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) + containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers, container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) { ItemCount = itemCount, Equip = equip, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 0ea9d4cad..a1d69ce93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -49,9 +49,11 @@ namespace Barotrauma.Items.Components } } + public readonly NamedEvent OnContainedItemsChanged = new NamedEvent(); + private bool alwaysContainedItemsSpawned; - public ItemInventory Inventory; + public readonly ItemInventory Inventory; private readonly List activeContainedItems = new List(); @@ -187,6 +189,16 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } + + /// + /// Can be used by status effects to lock the inventory + /// + public bool Locked + { + get { return Inventory.Locked; } + set { Inventory.Locked = value; } + } + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); @@ -214,9 +226,7 @@ namespace Barotrauma.Items.Components } private ImmutableHashSet containableItemIdentifiers; - public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; - - public override bool RecreateGUIOnResolutionChange => true; + public ImmutableHashSet ContainableItemIdentifiers => containableItemIdentifiers; public List ContainableItems { get; } @@ -347,6 +357,7 @@ namespace Barotrauma.Items.Components //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + OnContainedItemsChanged.Invoke(this); } public override void Move(Vector2 amount, bool ignoreContacts = false) @@ -360,6 +371,7 @@ namespace Barotrauma.Items.Components //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + OnContainedItemsChanged.Invoke(this); } public bool CanBeContained(Item item) @@ -496,7 +508,7 @@ namespace Barotrauma.Items.Components return false; } } - if (AutoInteractWithContained && character.SelectedConstruction == null) + if (AutoInteractWithContained && character.SelectedItem == null) { foreach (Item contained in Inventory.AllItems) { @@ -510,7 +522,15 @@ namespace Barotrauma.Items.Components var abilityItem = new AbilityItemContainer(item); character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem); - return base.Select(character); + if (item.ParentInventory?.Owner == character) + { + //can't select ItemContainers in the character's inventory (the inventory is drawn by hovering the cursor over the inventory slot, not as a GUIFrame) + return false; + } + else + { + return base.Select(character); + } } public override bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs index c1cb4be89..4d0f92aed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs @@ -19,8 +19,7 @@ namespace Barotrauma.Items.Components public override bool Select(Character character) { if (character == null || character.LockHands || character.Removed || !(character.AnimController is HumanoidAnimController)) return false; - - character.AnimController.Anim = AnimController.Animation.Climbing; + character.AnimController.StartClimbing(); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 84ad8903e..e11973068 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -36,6 +36,7 @@ namespace Barotrauma.Items.Components private readonly List limbPositions = new List(); private Direction dir; + public Direction Direction => dir; //the position where the user walks to when using the controller //(relative to the position of the item) @@ -128,6 +129,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")] + public bool IsSecondaryItem + { + get; + private set; + } + public Controller(Item item, ContentXElement element) : base(item, element) { @@ -150,7 +158,7 @@ namespace Barotrauma.Items.Components if (user == null || user.Removed - || user.SelectedConstruction != item + || !user.IsAnySelectedItem(item) || item.ParentInventory != null || !user.CanInteractWith(item) || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater) @@ -165,7 +173,7 @@ namespace Barotrauma.Items.Components return; } - user.AnimController.Anim = AnimController.Animation.UsingConstruction; + user.AnimController.StartUsingItem(); if (userPos != Vector2.Zero) { @@ -186,32 +194,34 @@ namespace Barotrauma.Items.Components } else { - diff.Y = 0.0f; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled) + // Secondary items (like ladders or chairs) will control the character position over primary items + // Only control the character position if the character doesn't have another secondary item already controlling it + if (!user.HasSelectedAnotherSecondaryItem(Item)) { - if (Math.Abs(diff.X) > 20.0f) + diff.Y = 0.0f; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled) { - //wait for the character to walk to the correct position - return; + if (Math.Abs(diff.X) > 20.0f) + { + //wait for the character to walk to the correct position + return; + } + else if (Math.Abs(diff.X) > 0.1f) + { + //aim to keep the collider at the correct position once close enough + user.AnimController.Collider.LinearVelocity = new Vector2( + diff.X * 0.1f, + user.AnimController.Collider.LinearVelocity.Y); + } } - else if (Math.Abs(diff.X) > 0.1f) - { - //aim to keep the collider at the correct position once close enough - user.AnimController.Collider.LinearVelocity = new Vector2( - diff.X * 0.1f, - user.AnimController.Collider.LinearVelocity.Y); - } - } - else - { - if (Math.Abs(diff.X) > 10.0f) + else if (Math.Abs(diff.X) > 10.0f) { user.AnimController.TargetMovement = Vector2.Normalize(diff); user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; return; } + user.AnimController.TargetMovement = Vector2.Zero; } - user.AnimController.TargetMovement = Vector2.Zero; UserInCorrectPosition = true; } } @@ -220,9 +230,16 @@ namespace Barotrauma.Items.Components if (limbPositions.Count == 0) { return; } - user.AnimController.Anim = AnimController.Animation.UsingConstruction; + user.AnimController.StartUsingItem(); - user.AnimController.ResetPullJoints(); + if (user.SelectedItem != null) + { + user.AnimController.ResetPullJoints(l => l.IsLowerBody); + } + else + { + user.AnimController.ResetPullJoints(); + } if (dir != 0) { user.AnimController.TargetDir = dir; } @@ -230,7 +247,10 @@ namespace Barotrauma.Items.Components { Limb limb = user.AnimController.GetLimb(lb.LimbType); if (limb == null || !limb.body.Enabled) { continue; } - + // Don't move lower body limbs if there's another selected secondary item that should control them + if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; } + // Don't move hands if there's a selected primary item that should control them + if (!limb.IsLowerBody && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; } if (lb.AllowUsingLimb) { switch (lb.LimbType) @@ -247,12 +267,9 @@ namespace Barotrauma.Items.Components break; } } - limb.Disabled = true; - Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.Position * item.Scale; Vector2 diff = worldPosition - limb.WorldPosition; - limb.PullJointEnabled = true; limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff); } @@ -266,9 +283,7 @@ namespace Barotrauma.Items.Components { return false; } - - if (user == null || user.Removed || - user.SelectedConstruction != item || !user.CanInteractWith(item)) + if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item)) { user = null; return false; @@ -290,46 +305,44 @@ namespace Barotrauma.Items.Components } lastUsed = Timing.TotalTime; - - ApplyStatusEffects(ActionType.OnUse, 1.0f, activator); - + ApplyStatusEffects(ActionType.OnUse, 1.0f, activator); return true; } public override bool SecondaryUse(float deltaTime, Character character = null) { - if (this.user != character) + if (user != character) { return false; } - - if (this.user == null || character.Removed || - this.user.SelectedConstruction != item || !character.CanInteractWith(item)) + if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item)) + { + user = null; + return false; + } + if (character == null) { - this.user = null; return false; } - if (character == null) return false; focusTarget = GetFocusTarget(); + if (focusTarget == null) { Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y); - Vector2 offset = character.CursorWorldPosition - centerPos; offset.Y = -offset.Y; - targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset)); return false; } character.ViewTarget = focusTarget; + #if CLIENT if (character == Character.Controlled && cam != null) { Lights.LightManager.ViewTarget = focusTarget; cam.TargetPos = focusTarget.WorldPosition; - cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected * focusTarget.OffsetOnSelectedMultiplier, deltaTime * 10.0f); HideHUDs(true); } @@ -338,16 +351,12 @@ namespace Barotrauma.Items.Components if (!character.IsRemotePlayer || character.ViewTarget == focusTarget) { Vector2 centerPos = new Vector2(focusTarget.WorldRect.Center.X, focusTarget.WorldRect.Center.Y); - - Turret turret = focusTarget.GetComponent(); - if (turret != null) + if (focusTarget.GetComponent() is { } turret) { centerPos = new Vector2(focusTarget.WorldRect.X + turret.TransformedBarrelPos.X, focusTarget.WorldRect.Y - turret.TransformedBarrelPos.Y); } - Vector2 offset = character.CursorWorldPosition - centerPos; offset.Y = -offset.Y; - targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset)); } return true; @@ -425,9 +434,10 @@ namespace Barotrauma.Items.Components humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; } - if (character.SelectedConstruction == this.item) { character.SelectedConstruction = null; } + if (character.SelectedItem == item) { character.SelectedItem = null; } + if (character.SelectedSecondaryItem == item) { character.SelectedSecondaryItem = null; } - character.AnimController.Anim = AnimController.Animation.None; + character.AnimController.StopUsingItem(); if (character == Character.Controlled) { HideHUDs(false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 9ba2ef20d..aa469f8a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -39,8 +40,6 @@ namespace Barotrauma.Items.Components [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] public float DeconstructionSpeed { get; set; } - public override bool RecreateGUIOnResolutionChange => true; - public Deconstructor(Item item, ContentXElement element) : base(item, element) { @@ -62,11 +61,18 @@ namespace Barotrauma.Items.Components inputContainer = containers[0]; outputContainer = containers[1]; +#if CLIENT + Identifier eventIdentifier = new Identifier(nameof(Deconstructor)); + inputContainer.OnContainedItemsChanged.RegisterOverwriteExisting(eventIdentifier, OnItemSlotsChanged); +#endif + OnItemLoadedProjSpecific(); } partial void OnItemLoadedProjSpecific(); + partial void OnItemSlotsChanged(ItemContainer container); + public override void Update(float deltaTime, Camera cam) { MoveInputQueue(); @@ -88,7 +94,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, 1.0f); + progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor); float tinkeringStrength = 0f; if (repairable.IsTinkering) @@ -114,7 +120,7 @@ namespace Barotrauma.Items.Components { if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; } var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => - (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) && + it.IsValidDeconstructor(item) && (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier == r))))).ToList(); ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); @@ -132,9 +138,7 @@ namespace Barotrauma.Items.Components var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } - var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => - it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)).ToList(); - + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -197,18 +201,18 @@ namespace Barotrauma.Items.Components foreach (DeconstructItem deconstructProduct in products) { - CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier); + CreateDeconstructProduct(deconstructProduct, inputItems, (int)(amountMultiplier * deconstructProduct.Amount)); } } else { foreach (DeconstructItem deconstructProduct in validDeconstructItems) { - CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier); + CreateDeconstructProduct(deconstructProduct, inputItems, (int)(amountMultiplier * deconstructProduct.Amount)); } } - void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems, float amountMultiplier) + void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems, int amount) { float percentageHealth = targetItem.Condition / targetItem.MaxCondition; @@ -276,11 +280,11 @@ namespace Barotrauma.Items.Components user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemDeconstructedInventory); } - int amount = (int)amountMultiplier; for (int i = 0; i < amount; i++) { Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { + spawnedItem.SpawnedInCurrentOutpost = item.SpawnedInCurrentOutpost; spawnedItem.StolenDuringRound = targetItem.StolenDuringRound; spawnedItem.AllowStealing = targetItem.AllowStealing; for (int i = 0; i < outputContainer.Capacity; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 1596f3c25..13f858f37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) { - float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); + float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * voltageFactor; float condition = item.Condition / item.MaxCondition; // Broken engine makes more noise. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 1d48a1378..9047d2f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -76,8 +76,6 @@ namespace Barotrauma.Items.Components get { return outputContainer; } } - public override bool RecreateGUIOnResolutionChange => true; - private float progressState; private readonly Dictionary fabricationLimits = new Dictionary(); @@ -305,7 +303,7 @@ namespace Barotrauma.Items.Components float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; - timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, 1.0f); + timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, MaxOverVoltageFactor); UpdateRequiredTimeProjSpecific(); @@ -371,8 +369,7 @@ namespace Barotrauma.Items.Components var availableItems = availableIngredients[requiredPrefab.Identifier]; var availableItem = availableItems.FirstOrDefault(potentialPrefab => { - return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && - potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); }); if (availableItem == null) { continue; } @@ -556,8 +553,20 @@ namespace Barotrauma.Items.Components const int MaxCraftingSkill = 100; + //having a higher-than-100 skill (e.g. due to talents) gives +1 quality quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; - quality += FabricationDegreeOfSuccess(user, fabricatedItem.RequiredSkills) >= 0.5f ? 1 : 0; + foreach (var skill in fabricatedItem.RequiredSkills) + { + //+1 quality if the character's skill level is >20% from the min requirement towards max skill + //e.g. if the skill requirement is 10 -> 28 + //40 -> 52 + //90 -> 92 + float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f); + if (user.GetSkillLevel(skill.Identifier) > skillRequirement) + { + quality += 1; + } + } return quality; } @@ -604,8 +613,7 @@ namespace Barotrauma.Items.Components var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; foreach (Item availablePrefab in availablePrefabs) { - if (availablePrefab.ConditionPercentage / 100.0f >= requiredItem.MinCondition && - availablePrefab.ConditionPercentage / 100.0f <= requiredItem.MaxCondition) + if (requiredItem.IsConditionSuitable(availablePrefab.ConditionPercentage)) { availablePrefabsAmount++; } @@ -637,10 +645,13 @@ namespace Barotrauma.Items.Components if (skills.Length == 0) { return 1.0f; } if (character == null) { return 0.0f; } - float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); - float average = skillSum / skills.Length; - - return (average + 100.0f) / 2.0f / 100.0f; + float minDegreeOfSuccess = 1.0f; + foreach (var skill in skills) + { + float characterLevel = character.GetSkillLevel(skill.Identifier); + minDegreeOfSuccess = Math.Min(minDegreeOfSuccess, (characterLevel - (skill.Level * SkillRequirementMultiplier) + 100.0f) / 2.0f / 100.0f); + } + return minDegreeOfSuccess; } public override float GetSkillMultiplier() @@ -648,13 +659,16 @@ namespace Barotrauma.Items.Components return SkillRequirementMultiplier; } + + private readonly HashSet linkedInventories = new HashSet(); + private void RefreshAvailableIngredients() { Character user = this.user; #if CLIENT user ??= Character.Controlled; #endif - + linkedInventories.Clear(); List itemList = new List(); itemList.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) @@ -674,6 +688,7 @@ namespace Barotrauma.Items.Components itemContainer = deconstructor.OutputContainer; } + linkedInventories.Add(itemContainer.Inventory); itemList.AddRange(itemContainer.Inventory.AllItems); } } @@ -688,6 +703,7 @@ namespace Barotrauma.Items.Components if (user?.Inventory != null) { itemList.AddRange(user.Inventory.AllItems); + linkedInventories.Add(user.Inventory); } availableIngredients.Clear(); foreach (Item item in itemList) @@ -720,9 +736,7 @@ namespace Barotrauma.Items.Components var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => { - return !usedItems.Contains(potentialPrefab) && - potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && - potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); }); if (availablePrefab == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 901359d64..47609785a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -52,7 +51,7 @@ namespace Barotrauma.Items.Components return; } - CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, 1.0f) * generatedAmount * 100.0f; + CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, MaxOverVoltageFactor) * generatedAmount * 100.0f; float conditionMult = item.Condition / item.MaxCondition; //100% condition = 100% oxygen //50% condition = 25% oxygen diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index bfd0b23fb..db2eae084 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null) { return; } - float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f); + float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 4d9561116..4630a0bf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -11,6 +11,8 @@ namespace Barotrauma.Items.Components { const float NetworkUpdateIntervalHigh = 0.5f; + const float TemperatureBoostAmount = 20; + //the rate at which the reactor is being run on (higher rate -> higher temperature) private float fissionRate; @@ -46,6 +48,11 @@ namespace Barotrauma.Items.Components private Vector2 optimalFissionRate, allowedFissionRate; private Vector2 optimalTurbineOutput, allowedTurbineOutput; + private float? signalControlledTargetFissionRate, signalControlledTargetTurbineOutput; + private double lastReceivedFissionRateSignalTime, lastReceivedTurbineOutputSignalTime; + + private float temperatureBoost; + private bool _powerOn; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] @@ -226,7 +233,7 @@ namespace Barotrauma.Items.Components // (= bots turn autotemp back on when leaving the reactor) if (LastAIUser != null) { - if (LastAIUser.SelectedConstruction != item && LastAIUser.CanInteractWith(item)) + if (LastAIUser.SelectedItem != item && LastAIUser.CanInteractWith(item)) { AutoTemp = true; if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } @@ -241,6 +248,34 @@ namespace Barotrauma.Items.Components } #endif + if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1) + { + TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); +#if CLIENT + FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; +#endif + } + else + { + signalControlledTargetFissionRate = null; + } + if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1) + { + TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f); +#if CLIENT + TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; +#endif + } + else + { + signalControlledTargetTurbineOutput = null; + } + + static float adjustValueWithoutOverShooting(float current, float target, float speed) + { + return target < current ? Math.Max(target, current - speed) : Math.Min(target, current + speed); + } + prevAvailableFuel = AvailableFuel; ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -270,7 +305,10 @@ namespace Barotrauma.Items.Components float temperatureDiff = (heatAmount - turbineOutput) - Temperature; Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); - //if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime; + temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime); +#if CLIENT + temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f; +#endif FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime); @@ -438,7 +476,7 @@ namespace Barotrauma.Items.Components private float GetGeneratedHeat(float fissionRate) { - return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; + return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f + temperatureBoost; } /// @@ -486,13 +524,15 @@ namespace Barotrauma.Items.Components if (temperature > allowedTemperature.Y) { item.SendSignal("1", "meltdown_warning"); - //faster meltdown if the item is in a bad condition - meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); - - if (meltDownTimer > MeltdownDelay) + if (!item.InvulnerableToDamage) { - MeltDown(); - return; + //faster meltdown if the item is in a bad condition + meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); + if (meltDownTimer > MeltdownDelay) + { + MeltDown(); + return; + } } } else @@ -505,7 +545,7 @@ namespace Barotrauma.Items.Components { fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); #if SERVER - if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item) + if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedItem == item) { GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime); } @@ -705,7 +745,7 @@ namespace Barotrauma.Items.Components { if (lastUser != null && lastUser != character && lastUser != LastAIUser) { - if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam) + if (lastUser.SelectedItem == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogReactorTaken").Value, null, 0.0f, "reactortaken".ToIdentifier(), 10.0f); } @@ -797,30 +837,31 @@ namespace Barotrauma.Items.Components AutoTemp = false; TargetFissionRate = 0.0f; TargetTurbineOutput = 0.0f; - if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } + registerUnsentChanges(); } break; case "set_fissionrate": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { - TargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f); - if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } -#if CLIENT - FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; -#endif + signalControlledTargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f); + lastReceivedFissionRateSignalTime = Timing.TotalTime; + registerUnsentChanges(); } break; case "set_turbineoutput": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { - TargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f); - if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } -#if CLIENT - TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; -#endif + signalControlledTargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f); + lastReceivedTurbineOutputSignalTime = Timing.TotalTime; + registerUnsentChanges(); } break; } + + void registerUnsentChanges() + { + if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index d60aad468..077ae053f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -64,6 +64,8 @@ namespace Barotrauma.Items.Components private const float MinZoom = 1.0f, MaxZoom = 4.0f; private float zoom = 1.0f; + /// Accessed through event actions. Do not remove even if there are no references in code. + public bool UseDirectionalPing => useDirectionalPing; private bool useDirectionalPing = false; private Vector2 pingDirection = new Vector2(1.0f, 0.0f); private bool useMineralScanner; @@ -113,13 +115,34 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. " + - "Only available in-game when the Item has no Steering component.")] - public bool HasMineralScanner { get; set; } + private bool hasMineralScanner; + + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. ")] + public bool HasMineralScanner + { + get => hasMineralScanner; + set + { +#if CLIENT + if (controlContainer != null && !hasMineralScanner && value) + { + AddMineralScannerSwitchToGUI(); + } +#endif + hasMineralScanner = value; + } + } public float Zoom { get { return zoom; } + set + { + zoom = MathHelper.Clamp(value, MinZoom, MaxZoom); +#if CLIENT + zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); +#endif + } } public Mode CurrentMode @@ -144,8 +167,6 @@ namespace Barotrauma.Items.Components } } - public override bool RecreateGUIOnResolutionChange => true; - public Sonar(Item item, ContentXElement element) : base(item, element) { @@ -396,17 +417,17 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(currentMode == Mode.Active); + msg.WriteBoolean(currentMode == Mode.Active); if (currentMode == Mode.Active) { msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8); - msg.Write(useDirectionalPing); + msg.WriteBoolean(useDirectionalPing); if (useDirectionalPing) { float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } - msg.Write(useMineralScanner); + msg.WriteBoolean(useMineralScanner); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index a04b1eb25..75776f814 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -166,8 +166,6 @@ namespace Barotrauma.Items.Components set { posToMaintain = value; } } - public override bool RecreateGUIOnResolutionChange => true; - struct ObstacleDebugInfo { public Vector2 Point1; @@ -301,7 +299,7 @@ namespace Barotrauma.Items.Components float userSkill = 0.0f; if (user != null && controlledSub != null && - (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction))) + (user.SelectedItem == item || item.linkedTo.Contains(user.SelectedItem))) { userSkill = user.GetSkillLevel("helm") / 100.0f; } @@ -333,7 +331,7 @@ namespace Barotrauma.Items.Components { showIceSpireWarning = false; if (user != null && user.Info != null && - user.SelectedConstruction == item && + user.SelectedItem == item && controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f) { IncreaseSkillLevel(user, deltaTime); @@ -389,7 +387,7 @@ namespace Barotrauma.Items.Components } // if our tactical AI pilot has left, revert back to maintaining position - if (navigateTactically && (user == null || user.SelectedConstruction != item)) + if (navigateTactically && (user == null || user.SelectedItem != item)) { navigateTactically = false; AIRamTimer = 0f; @@ -722,7 +720,7 @@ namespace Barotrauma.Items.Components character.AIController.SteeringManager.Reset(); if (objective.Override) { - if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) + if (user != character && user != null && user.SelectedItem == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogSteeringTaken").Value, null, 0.0f, "steeringtaken".ToIdentifier(), 10.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 46eac6dca..67c2f4a94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components set { maxRechargeSpeed = Math.Max(value, 1.0f); } } - [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")] public float RechargeSpeed { get { return rechargeSpeed; } @@ -117,9 +117,6 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.Yes, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] public bool ExponentialRechargeSpeed { get; set; } - [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, IsPropertySaveable.Yes)] - public float RechargeAdjustSpeed { get; set; } - private float efficiency; [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, IsPropertySaveable.Yes, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] public float Efficiency diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 388b719fc..8c15f3019 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -243,8 +243,13 @@ namespace Barotrauma.Items.Components //damage the item if voltage is too high (except if running as a client) float prevCondition = item.Condition; - item.Condition -= deltaTime * 10.0f; - + //some randomness to prevent all junction boxes from breaking at the same time + if (Rand.Range(0.0f, 1.0f) < 0.01f) + { + //damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time) + float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition); + item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor; + } if (item.Condition <= 0.0f && prevCondition > 0.0f) { overloadCooldownTimer = OverloadCooldown; @@ -273,7 +278,8 @@ namespace Barotrauma.Items.Components public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) { - return conn == powerOut ? PowerConsumption + ExtraLoad : 0; + //not used in the vanilla game (junction boxes or relays don't output power) + return conn == powerOut ? MathHelper.Max(-(PowerConsumption + ExtraLoad), 0) : 0; } public override bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index f0dbf6fcb..e355ccb14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -94,6 +94,11 @@ namespace Barotrauma.Items.Components protected Connection powerIn, powerOut; + /// + /// Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the device can function when it's being overvolted + /// + protected const float MaxOverVoltageFactor = 2.0f; + protected virtual PowerPriority Priority { get { return PowerPriority.Default; } } [Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " + @@ -685,43 +690,21 @@ namespace Barotrauma.Items.Components } /// - /// Efficient method to retrieve the batteries connected to the device + /// Returns a list of batteries directly connected to the item /// - /// All connected PowerContainers - protected List GetConnectedBatteries(bool outputOnly = true) + protected List GetDirectlyConnectedBatteries() { List batteries = new List(); - GridInfo supplyingGrid = null; - - //Determine supplying grid, prefer PowerIn connection - if (powerIn != null) + if (item.Connections == null || powerIn == null) { return batteries; } + foreach (Connection recipient in powerIn.Recipients) { - if (powerIn.Grid != null) + if (!recipient.IsPower || !recipient.IsOutput) { continue; } + var battery = recipient.Item?.GetComponent(); + if (battery != null) { - supplyingGrid = powerIn.Grid; + batteries.Add(battery); } } - else if (powerOut != null) - { - if (powerOut.Grid != null) - { - supplyingGrid = powerOut.Grid; - } - } - - if (supplyingGrid != null) - { - //Iterate through all connections to fine powerContainers - foreach (Connection c in supplyingGrid.Connections) - { - PowerContainer pc = c.Item.GetComponent(); - if (pc != null && (!outputOnly || pc.powerOut == c)) - { - batteries.Add(pc); - } - } - } - return batteries; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9c281f00d..b2d26005a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -244,6 +244,13 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool DamageDoors + { + get; + set; + } + public bool IsStuckToTarget => StickTarget != null; private Category originalCollisionCategories; @@ -288,7 +295,7 @@ namespace Barotrauma.Items.Components } } - private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f) + private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f) { Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); @@ -299,7 +306,7 @@ namespace Barotrauma.Items.Components // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. - Use(character: null); + Use(character: null, launchImpulseModifier); // Set user for normal projectiles to work properly. User = user; if (Item.Removed) { return; } @@ -312,7 +319,7 @@ namespace Barotrauma.Items.Components } } - public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f) + public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f, float launchImpulseModifier = 0f) { //add the limbs of the shooter to the list of bodies to be ignored //so that the player can't shoot himself @@ -320,7 +327,7 @@ namespace Barotrauma.Items.Components Vector2 projectilePos = weaponPos; //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking, - customPredicate: (Fixture f) => { return !IgnoredBodies.Contains(f.Body); }) == null) + customPredicate: (Fixture f) => { return IgnoredBodies == null || !IgnoredBodies.Contains(f.Body); }) == null) { //no obstacles -> we can spawn the projectile at the barrel projectilePos = spawnPos; @@ -334,7 +341,7 @@ namespace Barotrauma.Items.Components projectilePos = newPos; } } - Launch(user, projectilePos, rotation, damageMultiplier); + Launch(user, projectilePos, rotation, damageMultiplier, launchImpulseModifier); if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER @@ -344,7 +351,7 @@ namespace Barotrauma.Items.Components } } - public bool Use(Character character = null) + public bool Use(Character character = null, float launchImpulseModifier = 0f) { if (character != null && !characterUsable) { return false; } @@ -379,7 +386,7 @@ namespace Barotrauma.Items.Components else { item.body.SetTransform(item.body.SimPosition, launchAngle); - float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); + float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); } @@ -723,7 +730,7 @@ namespace Barotrauma.Items.Components private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact) { if (User != null && User.Removed) { User = null; return false; } - if (IgnoredBodies.Contains(target.Body)) { return false; } + if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -828,7 +835,7 @@ namespace Barotrauma.Items.Components private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { if (User != null && User.Removed) { User = null; } - if (IgnoredBodies.Contains(target.Body)) { return false; } + if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -851,7 +858,7 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { - if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) + if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User)) { return false; } @@ -870,9 +877,20 @@ namespace Barotrauma.Items.Components else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem) { if (targetItem.Removed) { return false; } - if (Attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0) + if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { - attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); + attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); +#if CLIENT + if (attackResult.Damage > 0.0f) + { + Character.Controlled?.UpdateHUDProgressBar(targetItem, + targetItem.WorldPosition, + targetItem.Condition / targetItem.MaxCondition, + emptyColor: GUIStyle.HealthBarColorLow, + fullColor: GUIStyle.HealthBarColorHigh, + textTag: targetItem.Name); + } +#endif } } else if (target.Body.UserData is IDamageable damageable) @@ -1056,7 +1074,7 @@ namespace Barotrauma.Items.Components item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; } } - IgnoredBodies.Clear(); + IgnoredBodies?.Clear(); } private void StickToTarget(Body targetBody, Vector2 axis) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 0d3dd454c..fbdb9e7c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -29,25 +29,14 @@ namespace Barotrauma.Items.Components FirepowerMultiplier, StrikingPowerMultiplier, StrikingSpeedMultiplier, - FiringRateMultiplier, - // unused as of now - AttackMultiplier, - // unused as of now - AttackSpeedMultiplier, - ForceDoorsOpenSpeedMultiplier, - RangedSpreadReduction, - ChargeSpeedMultiplier, - MovementSpeedMultiplier, - EffectivenessMultiplier, - PowerOutputMultiplier, - ConsumptionReductionMultiplier, + FiringRateMultiplier } private readonly Dictionary statValues = new Dictionary(); private int qualityLevel; - [Editable, Serialize(0, IsPropertySaveable.Yes)] + [Editable(MinValueInt = 0, MaxValueInt = MaxQuality), Serialize(0, IsPropertySaveable.Yes)] public int QualityLevel { get { return qualityLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 7f8fc6789..e486300c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -343,7 +343,7 @@ namespace Barotrauma.Items.Components { CurrentFixer.CheckTalents(AbilityEffectType.OnStopTinkering); } - CurrentFixer.AnimController.Anim = AnimController.Animation.None; + CurrentFixer.AnimController.StopUsingItem(); CurrentFixer = null; currentRepairItem = null; currentFixerAction = FixActions.None; @@ -430,7 +430,7 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (CurrentFixer != null && (CurrentFixer.SelectedConstruction != item || !CurrentFixer.CanInteractWith(item) || CurrentFixer.IsDead)) + if (CurrentFixer != null && (CurrentFixer.SelectedItem != item || !CurrentFixer.CanInteractWith(item) || CurrentFixer.IsDead)) { StopRepairing(CurrentFixer); return; @@ -502,7 +502,7 @@ namespace Barotrauma.Items.Components SteamAchievementManager.OnItemRepaired(item, CurrentFixer); CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); } - if (CurrentFixer?.SelectedConstruction == item) { CurrentFixer.SelectedConstruction = null; } + if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); wasBroken = false; StopRepairing(CurrentFixer); @@ -603,6 +603,9 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { if (Level.IsLoadedFriendlyOutpost) { return false; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode) { return false; } +#endif if (LastActiveTime > Timing.TotalTime) { return true; } foreach (ItemComponent ic in item.Components) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index bd3140234..35b4a9d10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -48,6 +48,8 @@ namespace Barotrauma.Items.Components { if (value == null) { return; } output = value; + //reactivate (we may not have been previously sending a signal, but might now) + IsActive = true; if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) { output = output.Substring(0, MaxOutputLength); @@ -63,6 +65,8 @@ namespace Barotrauma.Items.Components { if (value == null) { return; } falseOutput = value; + //reactivate (we may not have been previously sending a signal, but might now) + IsActive = true; if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) { falseOutput = falseOutput.Substring(0, MaxOutputLength); @@ -82,9 +86,14 @@ namespace Barotrauma.Items.Components public sealed override void Update(float deltaTime, Camera cam) { int receivedInputs = 0; + bool allInputsTimedOut = true; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } + if (timeSinceReceived[i] <= timeFrame) + { + allInputsTimedOut = false; + receivedInputs += 1; + } timeSinceReceived[i] += deltaTime; } @@ -93,7 +102,7 @@ namespace Barotrauma.Items.Components if (string.IsNullOrEmpty(signalOut)) { //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } + if (!state && allInputsTimedOut) { IsActive = false; } return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 3bad91591..0f9a8e60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -23,6 +23,9 @@ namespace Barotrauma.Items.Components private readonly HashSet wires; public IReadOnlyCollection Wires => wires; + private bool enumeratingWires; + private readonly HashSet removedWires = new HashSet(); + private readonly Item item; public readonly bool IsOutput; @@ -239,7 +242,14 @@ namespace Barotrauma.Items.Components } prevOtherConnection.recipientsDirty = true; } - wires.Remove(wire); + if (enumeratingWires) + { + removedWires.Add(wire); + } + else + { + wires.Remove(wire); + } recipientsDirty = true; } @@ -278,6 +288,7 @@ namespace Barotrauma.Items.Components public void SendSignal(Signal signal) { + enumeratingWires = true; foreach (var wire in wires) { Connection recipient = wire.OtherConnection(this); @@ -305,6 +316,12 @@ namespace Barotrauma.Items.Components } } } + enumeratingWires = false; + foreach (var removedWire in removedWires) + { + wires.Remove(removedWire); + } + removedWires.Clear(); } public void ClearConnections() @@ -317,13 +334,23 @@ namespace Barotrauma.Items.Components Powered.ChangedConnections.Add(c); } } - foreach (var wire in wires) { wire.RemoveConnection(this); recipientsDirty = true; } - wires.Clear(); + + if (enumeratingWires) + { + foreach (var wire in wires) + { + removedWires.Add(wire); + } + } + else + { + wires.Clear(); + } } public void InitializeFromLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 41a02b481..3954c06ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -179,7 +179,7 @@ namespace Barotrauma.Items.Components { UpdateProjSpecific(deltaTime); - if (user == null || user.SelectedConstruction != item) + if (user == null || user.SelectedItem != item) { #if SERVER if (user != null) { item.CreateServerEvent(this); } @@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components return; } - user.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * (((float)Timing.TotalTime / 10.0f) % 0.1f)); + user.AnimController.UpdateUseItem(!user.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * (((float)Timing.TotalTime / 10.0f) % 0.1f)); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime); - public override bool Select(Character picker) + public bool CanRewire() { //attaching wires to items with a body is not allowed //(signal items remove their bodies when attached to a wall) @@ -214,6 +214,15 @@ namespace Barotrauma.Items.Components { return false; } + return true; + } + + public override bool Select(Character picker) + { + if (!CanRewire()) + { + return false; + } user = picker; #if SERVER @@ -392,14 +401,14 @@ namespace Barotrauma.Items.Components msg.WriteVariableUInt32((uint)connection.Wires.Count); foreach (Wire wire in connection.Wires) { - msg.Write(wire?.Item == null ? (ushort)0 : wire.Item.ID); + msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID); } } - msg.Write((ushort)DisconnectedWires.Count); + msg.WriteUInt16((ushort)DisconnectedWires.Count); foreach (Wire disconnectedWire in DisconnectedWires) { - msg.Write(disconnectedWire.Item.ID); + msg.WriteUInt16(disconnectedWire.Item.ID); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index f2fe959e9..f5e52d074 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -155,7 +155,7 @@ namespace Barotrauma.Items.Components { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) //kind of hacky, we should probably add support for (string) arrays to SerializableEntityEditor so this wouldn't be needed - get { return signals == null ? "" : string.Join(";", signals); } + get { return signals == null ? string.Empty : string.Join(";", signals); } set { if (value == null) { return; } @@ -167,7 +167,31 @@ namespace Barotrauma.Items.Components } } - public override bool RecreateGUIOnResolutionChange => true; + private bool[] elementStates; + [Serialize("", IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)] + public string ElementStates + { + get { return elementStates == null ? string.Empty : string.Join(",", elementStates); } + set + { + if (value == null) { return; } + if (customInterfaceElementList.Count > 0) + { + string[] splitValues = value == "" ? Array.Empty() : value.Split(','); + for (int i = 0; i < customInterfaceElementList.Count && i < splitValues.Length; i++) + { + if (!bool.TryParse(splitValues[i], out bool val)) { continue; } + customInterfaceElementList[i].State = val; +#if CLIENT + if (uiElements != null && i < uiElements.Count && uiElements[i] is GUITickBox tickBox) + { + tickBox.Selected = val; + } +#endif + } + } + } + } private readonly List customInterfaceElementList = new List(); @@ -207,8 +231,10 @@ namespace Barotrauma.Items.Components } IsActive = true; InitProjSpecific(); + //load these here to ensure the UI elements (created in InitProjSpecific) are up-to-date Labels = element.GetAttributeString("labels", ""); Signals = element.GetAttributeString("signals", ""); + ElementStates = element.GetAttributeString("elementstates", ""); } private void UpdateLabels(string[] newLabels) @@ -386,6 +412,7 @@ namespace Barotrauma.Items.Components { labels = customInterfaceElementList.Select(ci => ci.Label).ToArray(); signals = customInterfaceElementList.Select(ci => ci.Signal).ToArray(); + elementStates = customInterfaceElementList.Select(ci => ci.State).ToArray(); return base.Save(parentElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index b44d136c2..32f151379 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -248,8 +248,19 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { - if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && IsOn && - (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && + CheckIfNeedsUpdate(); + } + + public void CheckIfNeedsUpdate() + { + if (!IsOn) + { + base.IsActive = false; + return; + } + + if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && + (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { lightBrightness = 1.0f; @@ -261,6 +272,10 @@ namespace Barotrauma.Items.Components Light.ParentSub = item.Submarine; #endif } + else + { + base.IsActive = true; + } } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index e278206b7..6ebfe3fc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -19,7 +19,8 @@ namespace Barotrauma.Items.Components Human = 1, Monster = 2, Wall = 4, - Any = Human | Monster | Wall, + Pet = 8, + Any = Human | Monster | Wall | Pet, } [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -253,7 +254,7 @@ namespace Barotrauma.Items.Components } } - if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Monster)) + if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Pet) || Target.HasFlag(TargetType.Monster)) { foreach (Character c in Character.CharacterList) { @@ -267,7 +268,11 @@ namespace Barotrauma.Items.Components { if (!Target.HasFlag(TargetType.Human)) { continue; } } - else if (!c.IsPet) + else if (c.IsPet) + { + if (!Target.HasFlag(TargetType.Pet)) { continue; } + } + else { if (!Target.HasFlag(TargetType.Monster)) { continue; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 0a0a3e691..5696c6280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -374,7 +374,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(isOn); + msg.WriteBoolean(isOn); } public void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 13409a48e..1d6188ebd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -106,11 +106,11 @@ namespace Barotrauma.Items.Components { case "set_text": case "signal_in": + if (string.IsNullOrEmpty(signal.value)) { return; } if (signal.value.Length > MaxMessageLength) { signal.value = signal.value.Substring(0, MaxMessageLength); } - string inputSignal = signal.value.Replace("\\n", "\n"); ShowOnDisplay(inputSignal, addToHistory: true, TextColor); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 5e70fcf9d..10fec01b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -58,6 +58,9 @@ namespace Barotrauma.Items.Components } } + public bool WaterDetected => isInWater; + public int WaterPercentage => GetWaterPercentage(item.CurrentHull); + public WaterDetector(Item item, ContentXElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index ec17bd33b..863210c6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -309,7 +309,7 @@ namespace Barotrauma.Items.Components if (nodes.Count == 0) { return; } Character user = item.ParentInventory?.Owner as Character; - editNodeDelay = (user?.SelectedConstruction == null) ? editNodeDelay - deltaTime : 0.5f; + editNodeDelay = (user?.SelectedItem == null) ? editNodeDelay - deltaTime : 0.5f; Submarine sub = item.Submarine; if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } @@ -369,7 +369,7 @@ namespace Barotrauma.Items.Components user.AnimController.Collider.ApplyForce(forceDir * user.Mass * 50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); if (diff.LengthSquared() > 50.0f * 50.0f) { - user.AnimController.UpdateUseItem(true, user.WorldPosition + pullBackDir * Math.Min(150.0f, diff.Length())); + user.AnimController.UpdateUseItem(!user.IsClimbing, user.WorldPosition + pullBackDir * Math.Min(150.0f, diff.Length())); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -428,7 +428,7 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null || character != Character.Controlled) { return false; } - if (character.SelectedConstruction != null) { return false; } + if (character.HasSelectedAnyItem) { return false; } #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen && !PlayerInput.PrimaryMouseButtonClicked()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index cf6c5cc42..df767e009 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using Barotrauma.Extensions; using FarseerPhysics.Dynamics; +using System.Collections.Immutable; namespace Barotrauma.Items.Components { @@ -21,12 +22,15 @@ namespace Barotrauma.Items.Components private float rotation, targetRotation; - private float reload, reloadTime; + private float reload, reloadTime, delayBetweenBurst; + private int shotsPerBurst, shotCounter; private float minRotation, maxRotation; private float launchImpulse; + private float damageMultiplier; + private Camera cam; private float angularVelocity; @@ -94,6 +98,16 @@ namespace Barotrauma.Items.Components get; set; } + + public bool flipFiringOffset; + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the firing offset will alternate from left to right (i.e. flipping the x-component of the offset each shot.)")] + public bool AlternatingFiringOffset + { + get; + set; + } + public Vector2 TransformedBarrelPos { get @@ -116,6 +130,20 @@ namespace Barotrauma.Items.Components set { reloadTime = value; } } + [Editable(1, 100), Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun.")] + public int ShotsPerBurst + { + get { return shotsPerBurst; } + set { shotsPerBurst = value; } + } + + [Editable(0.0f, 1000.0f, decimals: 3), Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload.")] + public float DelayBetweenBursts + { + get { return delayBetweenBurst; } + set { delayBetweenBurst = value; } + } + [Editable(0.1f, 10f), Serialize(1.0f, IsPropertySaveable.No, 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 { @@ -137,6 +165,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplies the damage the turret deals by this amount.")] + public float DamageMultiplier + { + get { return damageMultiplier; } + set { damageMultiplier = value; } + } + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { @@ -525,7 +560,7 @@ namespace Barotrauma.Items.Components UpdateLightComponents(); } - private void UpdateLightComponents() + public void UpdateLightComponents() { if (lightComponents != null) { @@ -602,9 +637,9 @@ namespace Barotrauma.Items.Components if (projectiles.Any()) { ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); - if (projectileContainer != null && projectileContainer.Item != item) - { - projectileContainer?.Item.Use(deltaTime, null); + if (projectileContainer != null && projectileContainer.Item != item) + { + projectileContainer?.Item.Use(deltaTime, null); } } else @@ -667,7 +702,7 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - List batteries = GetConnectedBatteries(); + List batteries = GetDirectlyConnectedBatteries(); float neededPower = GetPowerRequiredToShoot(); // tinkering is currently not factored into the common method as it is checked only when shooting @@ -764,6 +799,15 @@ namespace Barotrauma.Items.Components private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { reload = reloadTime; + if (ShotsPerBurst > 1) + { + shotCounter++; + if (shotCounter >= ShotsPerBurst) + { + reload += DelayBetweenBursts; + shotCounter = 0; + } + } reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease); if (user != null) @@ -773,6 +817,10 @@ namespace Barotrauma.Items.Components if (projectile != null) { + if (AlternatingFiringOffset) + { + flipFiringOffset = !flipFiringOffset; + } activeProjectiles.Add(projectile); projectile.Drop(null, setTransform: false); if (projectile.body != null) @@ -796,9 +844,9 @@ namespace Barotrauma.Items.Components projectileComponent.Attacker = projectileComponent.User = user; if (projectileComponent.Attack != null) { - projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength); + projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength); } - projectileComponent.Use(); + projectileComponent.Use(null, LaunchImpulse); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -1018,7 +1066,7 @@ namespace Barotrauma.Items.Components bool canShoot = true; if (!HasPowerToShoot()) { - List batteries = GetConnectedBatteries(); + List batteries = GetDirectlyConnectedBatteries(); float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) @@ -1104,7 +1152,7 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); - loadItemsObjective.ignoredContainerIdentifiers = new Identifier[] { ((MapEntity)containerItem).Prefab.Identifier }; + loadItemsObjective.ignoredContainerIdentifiers = ((MapEntity)containerItem).Prefab.Identifier.ToEnumerable().ToImmutableHashSet(); if (character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value, @@ -1260,9 +1308,13 @@ namespace Barotrauma.Items.Components } } float dist = Vector2.Distance(closestPoint, item.WorldPosition); + + //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell + closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); + if (dist > AIRange + 1000) { continue; } float dot = 0; - if (item.Submarine.Velocity != Vector2.Zero) + if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) { dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); } @@ -1293,7 +1345,7 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName)) + if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) { character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, identifier: "newtargetspotted".ToIdentifier(), @@ -1452,7 +1504,9 @@ namespace Barotrauma.Items.Components Vector2 transformedFiringOffset = Vector2.Zero; if (useOffset) { - transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-FiringOffset.Y, -FiringOffset.X) * item.Scale, -rotation); + Vector2 currOffSet = FiringOffset; + if (flipFiringOffset) { currOffSet.X = -currOffSet.X; } + transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -rotation); } return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y); } @@ -1562,6 +1616,7 @@ namespace Barotrauma.Items.Components targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); + UpdateLightComponents(); } public override void FlipY(bool relativeToSub) @@ -1583,6 +1638,7 @@ namespace Barotrauma.Items.Components targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); + UpdateLightComponents(); } public override void ReceiveSignal(Signal signal, Connection connection) @@ -1665,12 +1721,12 @@ namespace Barotrauma.Items.Components { if (TryExtractEventData(extraData, out EventData eventData)) { - msg.Write(eventData.Projectile.ID); + msg.WriteUInt16(eventData.Projectile.ID); msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); } else { - msg.Write((ushort)0); + msg.WriteUInt16((ushort)0); float wrappedTargetRotation = targetRotation; while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index ae4b5b1c6..57b4dc685 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -560,7 +560,7 @@ namespace Barotrauma.Items.Components } public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write((byte)Variant); + msg.WriteByte((byte)Variant); base.ServerEventWrite(msg, c, extraData); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 913529dbd..8ffc22676 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -969,14 +969,14 @@ namespace Barotrauma public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - msg.Write((byte)capacity); + msg.WriteByte((byte)capacity); for (int i = 0; i < capacity; i++) { msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize); for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++) { var item = slots[i].Items[j]; - msg.Write(item?.ID ?? (ushort)0); + msg.WriteUInt16(item?.ID ?? (ushort)0); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index dc755c2ce..c8c51358c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -100,6 +100,7 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; + private float waterDragCoefficient; public readonly XElement StaticBodyConfig; @@ -304,6 +305,10 @@ namespace Barotrauma { light.SetLightSourceTransform(); } + foreach (var turret in GetComponents()) + { + turret.UpdateLightComponents(); + } } #endif } @@ -418,6 +423,8 @@ namespace Barotrauma } } + public Color? HighlightColor; + [Serialize("", IsPropertySaveable.Yes)] @@ -459,7 +466,7 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.No)] /// - /// Can be used by status effects or conditionals to modify the sound range + /// Can be used by status effects or conditionals to modify the sight range /// public new float SightRange { @@ -519,6 +526,7 @@ namespace Barotrauma { float prevConditionPercentage = ConditionPercentage; healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); + RecalculateConditionValues(); condition = MaxCondition * prevConditionPercentage / 100.0f; RecalculateConditionValues(); } @@ -747,6 +755,9 @@ namespace Barotrauma get { return Prefab.Linkable; } } + public float WorldPositionX => WorldPosition.X; + public float WorldPositionY => WorldPosition.Y; + /// /// Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has been changed) /// @@ -807,6 +818,10 @@ namespace Barotrauma } } + public bool IsLadder { get; } + + public bool IsSecondaryItem { get; } + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -853,12 +868,14 @@ namespace Barotrauma SetActiveSprite(); + ContentXElement bodyElement = null; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "body": - float density = subElement.GetAttributeFloat("density", 10.0f); + bodyElement = subElement; + float density = subElement.GetAttributeFloat("density", Physics.NeutralDensity); float minDensity = subElement.GetAttributeFloat("mindensity", density); float maxDensity = subElement.GetAttributeFloat("maxdensity", density); if (minDensity < maxDensity) @@ -898,6 +915,7 @@ namespace Barotrauma body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false); body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f); body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); + body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); body.UserData = this; break; case "trigger": @@ -987,6 +1005,8 @@ namespace Barotrauma if (body != null) { body.Submarine = submarine; + waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", + GetComponent() != null || GetComponent() != null ? 0.1f : 1.0f); } //cache connections into a dictionary for faster lookups @@ -1014,6 +1034,9 @@ namespace Barotrauma qualityComponent = GetComponent(); + IsLadder = GetComponent() != null; + IsSecondaryItem = IsLadder || GetComponent() is { IsSecondaryItem: true }; + InitProjSpecific(); if (callOnItemLoaded) @@ -1522,7 +1545,7 @@ namespace Barotrauma return false; } - private bool ConditionalMatches(PropertyConditional conditional) + public bool ConditionalMatches(PropertyConditional conditional) { if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) { @@ -1890,10 +1913,22 @@ namespace Barotrauma if (needsWaterCheck) { + bool wasInWater = inWater; inWater = IsInWater(); bool waterProof = WaterProof; if (inWater) { + //the item has gone through the surface of the water + if (!wasInWater && CurrentHull != null && body != null && body.LinearVelocity.Y < -1.0f) + { + Splash(); + if (GetComponent() is not { IsActive: true }) + { + //slow the item down (not physically accurate, but looks good enough) + body.LinearVelocity *= 0.2f; + } + } + Item container = this.Container; while (!waterProof && container != null) { @@ -1921,7 +1956,8 @@ namespace Barotrauma } } - + partial void Splash(); + public void UpdateTransform() { if (body == null) { return; } @@ -2009,23 +2045,47 @@ namespace Barotrauma { float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height; float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width; - //forceFactor is 1.0f if the item is completely submerged, //and goes to 0.0f as the item goes through the surface forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f); - if (forceFactor <= 0.0f) return; + if (forceFactor <= 0.0f) { return; } } + bool moving = body.LinearVelocity.LengthSquared() > 0.001f; float volume = body.Mass / body.Density; + if (moving) + { + //measure velocity from the velocity of the front of the item and apply the drag to the other end to get the drag to turn the item the "pointy end first" - var uplift = -GameMain.World.Gravity * forceFactor * volume; + //a more "proper" (but more expensive) way to do this would be to e.g. calculate the drag separately for each edge of the fixture + //but since we define the "front" as the "pointy end", we can cheat a bit by using that, and actually even make the drag appear more realistic in some cases + //(e.g. a bullet with a rectangular fixture would be just as "aerodynamic" travelling backwards, but with this method we get it to turn the correct way) + Vector2 localFront = body.GetLocalFront(); + Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront); - Vector2 drag = body.LinearVelocity * volume; + float speed = frontVel.Length(); + float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity; + //very small drag on active projectiles to prevent affecting their trajectories much + if (body.FarseerBody.IsBullet) { drag *= 0.1f; } + Vector2 dragVec = -frontVel / speed * drag; - body.ApplyForce((uplift - drag) * 10.0f); + //apply the force slightly towards the back of the item to make it turn the front first + Vector2 back = body.FarseerBody.GetWorldPoint(-localFront * 0.01f); + body.ApplyForce(dragVec, back); + } + + //no need to apply buoyancy if the item is still and not light enough to float + if (moving || body.Density < 10.0f) + { + Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity; + body.ApplyForce(buoyancy); + } //apply simple angular drag - body.ApplyTorque(body.AngularVelocity * volume * -0.05f); + if (Math.Abs(body.AngularVelocity) > 0.0001f) + { + body.ApplyTorque(body.AngularVelocity * volume * -0.1f); + } } @@ -2131,7 +2191,7 @@ namespace Barotrauma /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent + public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true, Func connectionFilter = null) where T : ItemComponent { List connectedComponents = new List(); @@ -2147,6 +2207,7 @@ namespace Barotrauma foreach (Connection c in connectionPanel.Connections) { + if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; } var recipients = c.Recipients; foreach (Connection recipient in recipients) { @@ -2494,16 +2555,30 @@ namespace Barotrauma if (user != null) { - if (user.SelectedConstruction == this) + if (user.SelectedItem == this) { if (user.IsKeyHit(InputType.Select) || forceSelectKey) { - user.SelectedConstruction = null; + user.SelectedItem = null; + } + } + else if (user.SelectedSecondaryItem == this) + { + if (user.IsKeyHit(InputType.Select) || forceSelectKey) + { + user.SelectedSecondaryItem = null; } } else if (selected) { - user.SelectedConstruction = this; + if (IsSecondaryItem) + { + user.SelectedSecondaryItem = this; + } + else + { + user.SelectedItem = this; + } } } @@ -2809,77 +2884,77 @@ namespace Barotrauma var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.Write((byte)allProperties.FindIndex(p => p.property == property)); + msg.WriteByte((byte)allProperties.FindIndex(p => p.property == property)); } object value = property.GetValue(propertyOwner.obj); if (value is string stringVal) { - msg.Write(stringVal); + msg.WriteString(stringVal); } else if (value is Identifier idValue) { - msg.Write(idValue); + msg.WriteIdentifier(idValue); } else if (value is float floatVal) { - msg.Write(floatVal); + msg.WriteSingle(floatVal); } else if (value is int intVal) { - msg.Write(intVal); + msg.WriteInt32(intVal); } else if (value is bool boolVal) { - msg.Write(boolVal); + msg.WriteBoolean(boolVal); } else if (value is Color color) { - msg.Write(color.R); - msg.Write(color.G); - msg.Write(color.B); - msg.Write(color.A); + msg.WriteByte(color.R); + msg.WriteByte(color.G); + msg.WriteByte(color.B); + msg.WriteByte(color.A); } else if (value is Vector2 vector2) { - msg.Write(vector2.X); - msg.Write(vector2.Y); + msg.WriteSingle(vector2.X); + msg.WriteSingle(vector2.Y); } else if (value is Vector3 vector3) { - msg.Write(vector3.X); - msg.Write(vector3.Y); - msg.Write(vector3.Z); + msg.WriteSingle(vector3.X); + msg.WriteSingle(vector3.Y); + msg.WriteSingle(vector3.Z); } else if (value is Vector4 vector4) { - msg.Write(vector4.X); - msg.Write(vector4.Y); - msg.Write(vector4.Z); - msg.Write(vector4.W); + msg.WriteSingle(vector4.X); + msg.WriteSingle(vector4.Y); + msg.WriteSingle(vector4.Z); + msg.WriteSingle(vector4.W); } else if (value is Point point) { - msg.Write(point.X); - msg.Write(point.Y); + msg.WriteInt32(point.X); + msg.WriteInt32(point.Y); } else if (value is Rectangle rect) { - msg.Write(rect.X); - msg.Write(rect.Y); - msg.Write(rect.Width); - msg.Write(rect.Height); + msg.WriteInt32(rect.X); + msg.WriteInt32(rect.Y); + msg.WriteInt32(rect.Width); + msg.WriteInt32(rect.Height); } else if (value is Enum) { - msg.Write((int)value); + msg.WriteInt32((int)value); } else if (value is string[] a) { - msg.Write(a.Length); + msg.WriteInt32(a.Length); for (int i = 0; i < a.Length; i++) { - msg.Write(a[i] ?? ""); + msg.WriteString(a[i] ?? ""); } } else @@ -3292,10 +3367,8 @@ namespace Barotrauma item.PurchasedNewSwap = false; } - float condition = element.GetAttributeFloat("condition", item.MaxCondition); - item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); + item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); item.lastSentCondition = item.condition; - item.RecalculateConditionValues(); item.SetActiveSprite(); @@ -3453,7 +3526,8 @@ namespace Barotrauma foreach (Character character in Character.CharacterList) { - if (character.SelectedConstruction == this) { character.SelectedConstruction = null; } + if (character.SelectedItem == this) { character.SelectedItem = null; } + if (character.SelectedSecondaryItem == this) { character.SelectedSecondaryItem = null; } } Door door = GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index e97bb6481..59ee01824 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -14,6 +14,8 @@ namespace Barotrauma readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; + //number of items to output + public readonly int Amount; //minCondition does <= check, meaning that below or equal to min condition will be skipped. public readonly float MinCondition; //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. @@ -37,6 +39,7 @@ namespace Barotrauma public DeconstructItem(XElement element, Identifier parentDebugName) { ItemIdentifier = element.GetAttributeIdentifier("identifier", ""); + Amount = element.GetAttributeInt("amount", 1); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); @@ -50,6 +53,11 @@ namespace Barotrauma InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); } + + public bool IsValidDeconstructor(Item deconstructor) + { + return RequiredDeconstructor.Length == 0 || RequiredDeconstructor.Any(r => deconstructor.HasTag(r) || deconstructor.Prefab.Identifier == r); + } } class FabricationRecipe @@ -59,6 +67,10 @@ namespace Barotrauma public abstract IEnumerable ItemPrefabs { get; } public abstract UInt32 UintIdentifier { get; } + public abstract bool MatchesItem(Item item); + + public abstract ItemPrefab FirstMatchingPrefab { get; } + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition) { Amount = amount; @@ -70,17 +82,40 @@ namespace Barotrauma public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; + + public bool IsConditionSuitable(float conditionPercentage) + { + float normalizedCondition = conditionPercentage / 100.0f; + if (MathUtils.NearlyEqual(normalizedCondition, MinCondition) || MathUtils.NearlyEqual(normalizedCondition, MaxCondition)) + { + return true; + } + else if (normalizedCondition >= MinCondition && normalizedCondition <= MaxCondition) + { + return true; + } + return false; + } } public class RequiredItemByIdentifier : RequiredItem { public readonly Identifier ItemPrefabIdentifier; + public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab ?? throw new Exception($"No ItemPrefab with identifier or name \"{ItemPrefabIdentifier}\""); + public override UInt32 UintIdentifier { get; } public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + public override ItemPrefab FirstMatchingPrefab => ItemPrefab; + + public override bool MatchesItem(Item item) + { + return item?.Prefab.Identifier == ItemPrefabIdentifier; + } + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { ItemPrefabIdentifier = itemPrefab; @@ -92,10 +127,19 @@ namespace Barotrauma public class RequiredItemByTag : RequiredItem { public readonly Identifier Tag; + public override UInt32 UintIdentifier { get; } public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); + public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag)); + + public override bool MatchesItem(Item item) + { + if (item == null) { return false; } + return item.HasTag(Tag); + } + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { Tag = tag; @@ -191,10 +235,10 @@ namespace Barotrauma if (requiredItemIdentifier != Identifier.Empty) { var existing = requiredItems.FindIndex(r => - r is RequiredItemByIdentifier ri && - ri.ItemPrefabIdentifier == requiredItemIdentifier && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && - MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + r is RequiredItemByIdentifier ri && + ri.ItemPrefabIdentifier == requiredItemIdentifier && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing >= 0) { amount += requiredItems[existing].Amount; @@ -205,10 +249,10 @@ namespace Barotrauma else { var existing = requiredItems.FindIndex(r => - r is RequiredItemByTag rt && - rt.Tag == requiredItemTag && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && - MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + r is RequiredItemByTag rt && + rt.Tag == requiredItemTag && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing >= 0) { amount += requiredItems[existing].Amount; @@ -393,12 +437,106 @@ namespace Barotrauma private set; } + public readonly struct CommonnessInfo + { + public float Commonness + { + get + { + return commonness; + } + } + public float AbyssCommonness + { + get + { + return abyssCommonness ?? 0.0f; + } + } + public float CaveCommonness + { + get + { + return caveCommonness ?? Commonness; + } + } + public bool CanAppear + { + get + { + if (Commonness > 0.0f) { return true; } + if (AbyssCommonness > 0.0f) { return true; } + if (CaveCommonness > 0.0f) { return true; } + return false; + } + } + + public readonly float commonness; + public readonly float? abyssCommonness; + public readonly float? caveCommonness; + + public CommonnessInfo(XElement element) + { + this.commonness = Math.Max(element?.GetAttributeFloat("commonness", 0.0f) ?? 0.0f, 0.0f); + + float? abyssCommonness = null; + XAttribute abyssCommonnessAttribute = element?.GetAttribute("abysscommonness") ?? element?.GetAttribute("abyss"); + if (abyssCommonnessAttribute != null) + { + abyssCommonness = Math.Max(abyssCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f); + } + this.abyssCommonness = abyssCommonness; + + float? caveCommonness = null; + XAttribute caveCommonnessAttribute = element?.GetAttribute("cavecommonness") ?? element?.GetAttribute("cave"); + if (caveCommonnessAttribute != null) + { + caveCommonness = Math.Max(caveCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f); + } + this.caveCommonness = caveCommonness; + } + + public CommonnessInfo(float commonness, float? abyssCommonness, float? caveCommonness) + { + this.commonness = commonness; + this.abyssCommonness = abyssCommonness != null ? (float?)Math.Max(abyssCommonness.Value, 0.0f) : null; + this.caveCommonness = caveCommonness != null ? (float?)Math.Max(caveCommonness.Value, 0.0f) : null; + } + + public CommonnessInfo WithInheritedCommonness(CommonnessInfo? parentInfo) + { + return new CommonnessInfo(commonness, + abyssCommonness ?? parentInfo?.abyssCommonness, + caveCommonness ?? parentInfo?.caveCommonness); + } + + public CommonnessInfo WithInheritedCommonness(params CommonnessInfo?[] parentInfos) + { + CommonnessInfo info = this; + foreach (var parentInfo in parentInfos) + { + info = info.WithInheritedCommonness(parentInfo); + } + return info; + } + + public float GetCommonness(Level.TunnelType tunnelType) + { + if (tunnelType == Level.TunnelType.Cave) + { + return CaveCommonness; + } + else + { + return Commonness; + } + } + } + /// /// How likely it is for the item to spawn in a level of a given type. - /// Key = name of the LevelGenerationParameters (empty string = default value) /* TODO: empty string = default value???? */ - /// Value = commonness /// - public ImmutableDictionary LevelCommonness { get; private set; } + private ImmutableDictionary LevelCommonness { get; set; } public readonly struct FixedQuantityResourceInfo { @@ -669,22 +807,12 @@ namespace Barotrauma //only used if the item doesn't have a name/description defined in the currently selected language string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", ""); - //works the same as nameIdentifier, but just replaces the description - Identifier descriptionIdentifier = ConfigElement.GetAttributeIdentifier("descriptionidentifier", ""); - - if (string.IsNullOrEmpty(OriginalName)) - { - name = TextManager.Get(nameIdentifier.IsEmpty - ? $"EntityName.{Identifier}" - : $"EntityName.{nameIdentifier}", - $"EntityName.{fallbackNameIdentifier}"); - } - else if (Category.HasFlag(MapEntityCategory.Legacy)) - { - // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore - name = TextManager.Get(nameIdentifier.IsEmpty + name = TextManager.Get(nameIdentifier.IsEmpty ? $"EntityName.{Identifier}" - : $"EntityName.{nameIdentifier}"); + : $"EntityName.{nameIdentifier}", + $"EntityName.{fallbackNameIdentifier}"); + if (!string.IsNullOrEmpty(OriginalName)) + { name = name.Fallback(OriginalName); } @@ -727,27 +855,13 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); - if (Description.IsNullOrEmpty()) - { - if (descriptionIdentifier != Identifier.Empty) - { - Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); - } - else if (nameIdentifier == Identifier.Empty) - { - Description = TextManager.Get($"EntityDescription.{Identifier}"); - } - else - { - Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); - } - } + LoadDescription(ConfigElement); var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); AllowDroppingOnSwap = allowDroppingOnSwapWith.Any(); - var levelCommonness = new Dictionary(); + var levelCommonness = new Dictionary(); var levelQuantity = new Dictionary(); foreach (ContentXElement subElement in ConfigElement.Elements()) @@ -871,7 +985,7 @@ namespace Barotrauma { if (!levelCommonness.ContainsKey(levelName)) { - levelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + levelCommonness.Add(levelName, new CommonnessInfo(levelCommonnessElement)); } } else @@ -962,6 +1076,40 @@ namespace Barotrauma this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); } + public CommonnessInfo? GetCommonnessInfo(Level level) + { + CommonnessInfo? levelCommonnessInfo = GetValueOrNull(level.GenerationParams.Identifier); + CommonnessInfo? biomeCommonnessInfo = GetValueOrNull(level.LevelData.Biome.Identifier); + CommonnessInfo? defaultCommonnessInfo = GetValueOrNull(Identifier.Empty); + + if (levelCommonnessInfo.HasValue) + { + return levelCommonnessInfo?.WithInheritedCommonness(biomeCommonnessInfo, defaultCommonnessInfo); + } + else if (biomeCommonnessInfo.HasValue) + { + return biomeCommonnessInfo?.WithInheritedCommonness(defaultCommonnessInfo); + } + else if (defaultCommonnessInfo.HasValue) + { + return defaultCommonnessInfo; + } + + return null; + + CommonnessInfo? GetValueOrNull(Identifier identifier) + { + if (LevelCommonness.TryGetValue(identifier, out CommonnessInfo info)) + { + return info; + } + else + { + return null; + } + } + } + public float GetTreatmentSuitability(Identifier treatmentIdentifier) { return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; @@ -975,7 +1123,7 @@ namespace Barotrauma { string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}"; #if DEBUG - DebugConsole.ShowError(message); + DebugConsole.LogError(message); #else DebugConsole.AddWarning(message); GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message); @@ -1158,12 +1306,8 @@ namespace Barotrauma throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance"); } - private bool disposed = false; public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); Item.RemoveByPrefab(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index a74a686d4..04351e385 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -22,7 +23,7 @@ namespace Barotrauma public bool IgnoreInEditor { get; set; } - private Identifier[] excludedIdentifiers; + private ImmutableHashSet excludedIdentifiers; private RelationType type; @@ -60,11 +61,11 @@ namespace Barotrauma { if (value == null) return; - Identifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); } } - public Identifier[] Identifiers { get; private set; } + public ImmutableHashSet Identifiers { get; private set; } public string JoinedExcludedIdentifiers { @@ -73,27 +74,53 @@ namespace Barotrauma { if (value == null) return; - excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); } } public bool MatchesItem(Item item) { if (item == null) { return false; } - if (excludedIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } - return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); + if (excludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; } + foreach (var excludedIdentifier in excludedIdentifiers) + { + if (item.HasTag(excludedIdentifier)) { return false; } + } + if (Identifiers.Contains(item.Prefab.Identifier)) { return true; } + foreach (var identifier in Identifiers) + { + if (item.HasTag(identifier)) { return true; } + } + if (AllowVariants && !item.Prefab.VariantOf.IsEmpty) + { + if (Identifiers.Contains(item.Prefab.VariantOf)) { return true; } + } + return false; } public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } - if (excludedIdentifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id))) { return false; } - return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (AllowVariants && !itemPrefab.VariantOf.IsEmpty && itemPrefab.VariantOf == id)); + if (excludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; } + foreach (var excludedIdentifier in excludedIdentifiers) + { + if (itemPrefab.Tags.Contains(excludedIdentifier)) { return false; } + } + if (Identifiers.Contains(itemPrefab.Identifier)) { return true; } + foreach (var identifier in Identifiers) + { + if (itemPrefab.Tags.Contains(identifier)) { return true; } + } + if (AllowVariants && !itemPrefab.VariantOf.IsEmpty) + { + if (Identifiers.Contains(itemPrefab.VariantOf)) { return true; } + } + return false; } public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers) { - this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); - this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); + this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); statusEffects = new List(); } @@ -161,7 +188,7 @@ namespace Barotrauma new XAttribute("targetslot", TargetSlot), new XAttribute("allowvariants", AllowVariants)); - if (excludedIdentifiers.Length > 0) + if (excludedIdentifiers.Count > 0) { element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index c5c646150..193e7fdf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -84,7 +84,6 @@ namespace Barotrauma } } - private bool disposed = false; public override Sprite Sprite => null; @@ -102,9 +101,8 @@ namespace Barotrauma public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); + throw new InvalidOperationException( + $"{nameof(CoreEntityPrefab)}.{nameof(Dispose)} should never be called"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 8ab8ee918..329102f51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -33,8 +33,6 @@ namespace Barotrauma private readonly float? flashRange; private readonly string decal; private readonly float decalSize; - // used to apply friendly afflictions in an area without effects displaying - private readonly bool abilityExplosion; private readonly bool applyToSelf; private readonly float itemRepairStrength; @@ -70,6 +68,7 @@ namespace Barotrauma applyToSelf = element.GetAttributeBool("applytoself", true); + //the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects") bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true); sparks = element.GetAttributeBool("sparks", showEffects); shockwave = element.GetAttributeBool("shockwave", showEffects); @@ -131,8 +130,15 @@ namespace Barotrauma float displayRange = Attack.Range; if (damageSource is Item sourceItem) { - displayRange *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius); - Attack.DamageMultiplier *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage); + var launcher = sourceItem.GetComponent()?.Launcher; + displayRange *= + 1.0f + + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + + (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0); + Attack.DamageMultiplier *= + 1.0f + + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage) + + (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0); Attack.SourceItem ??= sourceItem; } @@ -203,7 +209,7 @@ namespace Barotrauma } } - if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f) && !abilityExplosion) + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && Attack.Afflictions.None()) { return; } @@ -293,12 +299,13 @@ namespace Barotrauma Dictionary distFactors = new Dictionary(); Dictionary damages = new Dictionary(); List modifiedAfflictions = new List(); + foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; } float dist = Vector2.Distance(limb.WorldPosition, worldPosition); - + //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = limb.body.GetMaxExtent(); @@ -313,17 +320,27 @@ namespace Barotrauma { distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); } - distFactors.Add(limb, distFactor); + if (distFactor > 0) + { + distFactors.Add(limb, distFactor); + } + } + foreach (Limb limb in distFactors.Keys) + { + if (!distFactors.TryGetValue(limb, out float distFactor)) { continue; } modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { - //previously the damage would be divided by the number of limbs (the intention was to prevent characters with more limbs taking more damage from explosions) - //that didn't work well on large characters like molochs and endworms: the explosions tend to only damage one or two of their limbs, and since the characters - //have lots of limbs, they tended to only take a fraction of the damage they should - - //now we just divide by 10, which keeps the damage to normal-sized characters roughly the same as before and fixes the large characters - modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / 10)); + // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor + // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. + float limbCountFactor = Math.Min(distFactors.Count, 15); + float dmgMultiplier = distFactor; + if (affliction.DivideByLimbCount) + { + dmgMultiplier /= limbCountFactor; + } + modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -368,7 +385,7 @@ namespace Barotrauma Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; - Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; + Vector2 impulsePoint = limb.SimPosition - limbDiff * limb.body.GetMaxExtent(); limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 3dcdcdcd8..fb0b0c1ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -26,6 +26,12 @@ namespace Barotrauma private set; } + /// + /// "Diagonal" gaps are used on sloped walls to allow characters to pass through them either horizontally or vertically. + /// Water still flows through them only horizontally or vertically + /// + public bool IsDiagonal { get; } + //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; @@ -136,12 +142,13 @@ namespace Barotrauma : this(rect, rect.Width < rect.Height, submarine) { } - public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) + public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, bool isDiagonal = false, ushort id = Entity.NullEntityID) : base(CoreEntityPrefab.GapPrefab, submarine, id) { this.rect = rect; flowForce = Vector2.Zero; IsHorizontal = isHorizontal; + IsDiagonal = isDiagonal; open = 1.0f; FindHulls(); @@ -671,15 +678,15 @@ namespace Barotrauma { foreach (Gap gap in gaps) { - if (gap.Open == 0.0f || gap.IsRoomToRoom) continue; + if (gap.Open == 0.0f || gap.IsRoomToRoom) { continue; } if (gap.ConnectedWall != null) { int sectionIndex = gap.ConnectedWall.FindSectionIndex(gap.Position); - if (sectionIndex > -1 && !gap.ConnectedWall.SectionBodyDisabled(sectionIndex)) continue; + if (sectionIndex > -1 && !gap.ConnectedWall.SectionBodyDisabled(sectionIndex)) { continue; } } - if (gap.IsHorizontal) + if (gap.IsHorizontal || gap.IsDiagonal) { if (worldPos.Y < gap.WorldRect.Y && worldPos.Y > gap.WorldRect.Y - gap.WorldRect.Height && Math.Abs(gap.WorldRect.Center.X - worldPos.X) < allowedOrthogonalDist) @@ -687,7 +694,7 @@ namespace Barotrauma return gap; } } - else + if (!gap.IsHorizontal || gap.IsDiagonal) { if (worldPos.X > gap.WorldRect.X && worldPos.X < gap.WorldRect.Right && Math.Abs(gap.WorldRect.Y - gap.WorldRect.Height / 2 - worldPos.Y) < allowedOrthogonalDist) @@ -759,7 +766,7 @@ namespace Barotrauma isHorizontal = horizontalAttribute.Value.ToString() == "true"; } - Gap g = new Gap(rect, isHorizontal, submarine, idRemap.GetOffsetId(element)) + Gap g = new Gap(rect, isHorizontal, submarine, id: idRemap.GetOffsetId(element)) { linkedToID = new List(), }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 9702e90be..3752320d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -759,7 +759,7 @@ namespace Barotrauma for (int i = start; i < end; i++) { msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); - msg.Write(BackgroundSections[i].Color.PackedValue); + msg.WriteUInt32(BackgroundSections[i].Color.PackedValue); } } #endregion @@ -994,7 +994,11 @@ namespace Barotrauma foreach (var gap in ConnectedGaps.Where(gap => gap.Open > 0)) { var distance = MathHelper.Max(Vector2.DistanceSquared(item.Position, gap.Position) / 1000, 1f); - item.body.ApplyForce((gap.LerpedFlowForce / distance) * deltaTime); + Vector2 force = (gap.LerpedFlowForce / distance) * deltaTime; + if (force.LengthSquared() > 0.01f) + { + item.body.ApplyForce(force); + } } } @@ -1545,7 +1549,7 @@ namespace Barotrauma var hull = new Hull(rect, submarine, idRemap.GetOffsetId(element)) { - WaterVolume = element.GetAttributeFloat("pressure", 0.0f) + WaterVolume = element.GetAttributeFloat("water", 0.0f) }; hull.linkedToID = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 574115e1f..ab5c17c8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -157,7 +157,7 @@ namespace Barotrauma public void Delete() { - Dispose(); + Prefabs.Remove(this); try { if (ContentPackage is { Files: { Length: 1 } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 298638c46..ee915e10e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -12,7 +12,9 @@ namespace Barotrauma public readonly bool IsEndBiome; public readonly float MinDifficulty; - public readonly float MaxDifficulty; + private readonly float maxDifficulty; + public float ActualMaxDifficulty => maxDifficulty; + public float AdjustedMaxDifficulty => maxDifficulty - 0.1f; public readonly ImmutableHashSet AllowedZones; @@ -31,7 +33,7 @@ namespace Barotrauma IsEndBiome = element.GetAttributeBool("endbiome", false); AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); - MaxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); + maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); } public static Identifier ParseIdentifier(ContentXElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index b71326832..c80309932 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -79,7 +79,7 @@ namespace Barotrauma set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int LevelObjectAmount { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 133da33e1..4daa4282a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -238,8 +238,8 @@ namespace Barotrauma //find the edge at the opposite side of the adjacent cell foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges) { - if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) < 0 && - otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType == CellType.Solid) + if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 && + otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { adjacentEdge = otherEdge; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 981ea9027..e864b0a9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using Voronoi2; @@ -24,6 +25,22 @@ namespace Barotrauma //all entities are disabled after they reach this depth public const int MaxEntityDepth = -1000000; public const float ShaftHeight = 1000.0f; + + /// + /// How far outside the boundaries of the level the water current that pushes subs towards the level starts + /// + public const float OutsideBoundsCurrentMargin = 30000.0f; + + /// + /// How far outside the boundaries of the level the strength of the current starts to increase exponentially + /// + public const float OutsideBoundsCurrentMarginExponential = 150000.0f; + + /// + /// How far outside the boundaries of the level the current stops submarines entirely + /// + public const float OutsideBoundsCurrentHardLimit = 200000.0f; + /// /// The level generator won't try to adjust the width of the main path above this limit. /// @@ -1955,7 +1972,7 @@ namespace Barotrauma List caveBranches = new List(); - var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 100, parentTunnel); + var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 150, parentTunnel); Tunnels.Add(tunnel); caveBranches.Add(tunnel); @@ -1972,7 +1989,7 @@ namespace Barotrauma bounds: caveArea); if (!branchSegments.Any()) { continue; } - var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 0, parentBranch); + var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch); Tunnels.Add(branch); caveBranches.Add(branch); } @@ -2437,16 +2454,27 @@ namespace Barotrauma public List ClusterLocations { get; } public TunnelType TunnelType { get; } - public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType) + private PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType, List resourceTags, List resourceIds, List clusterLocations) { - Id = id; + Id = id; Position = position; ShouldContainResources = shouldContainResources; - ResourceTags = new List(); - ResourceIds = new List(); - ClusterLocations = new List(); + ResourceTags = resourceTags; + ResourceIds = resourceIds; + ClusterLocations = clusterLocations; TunnelType = tunnelType; } + + public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType) + : this(id, position, shouldContainResources, tunnelType, new List(), new List(), new List()) + { + + } + + public PathPoint WithResources(bool containsResources) + { + return new PathPoint(Id, Position, containsResources, TunnelType, ResourceTags, ResourceIds, ClusterLocations); + } } public List AbyssResources { get; } = new List(); @@ -2485,22 +2513,26 @@ namespace Barotrauma // Such as the exploding crystals in The Great Sea private void GenerateItems() { - Identifier levelName = GenerationParams.Identifier; - float minCommonness = float.MaxValue, maxCommonness = float.MinValue; - List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>(); + var levelResources = new List<(ItemPrefab itemPrefab, ItemPrefab.CommonnessInfo commonnessInfo)>(); var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>(); + Vector2 commonnessRange = new Vector2(float.MaxValue, float.MinValue), caveCommonnessRange = new Vector2(float.MaxValue, float.MinValue); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier)) { - if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || - itemPrefab.LevelCommonness.TryGetValue(LevelData.Biome.Identifier, out commonness) || - itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness)) + if (itemPrefab.GetCommonnessInfo(this) is { CanAppear: true } commonnessInfo) { - if (commonness <= 0.0f) { continue; } - if (commonness < minCommonness) { minCommonness = commonness; } - if (commonness > maxCommonness) { maxCommonness = commonness; } - levelResources.Add((itemPrefab, commonness)); + if (commonnessInfo.Commonness > 0.0) + { + if (commonnessInfo.Commonness < commonnessRange.X) { commonnessRange.X = commonnessInfo.Commonness; } + if (commonnessInfo.Commonness > commonnessRange.Y) { commonnessRange.Y = commonnessInfo.Commonness; } + } + if (commonnessInfo.CaveCommonness > 0.0) + { + if (commonnessInfo.CaveCommonness < caveCommonnessRange.X) { caveCommonnessRange.X = commonnessInfo.CaveCommonness; } + if (commonnessInfo.CaveCommonness > caveCommonnessRange.Y) { caveCommonnessRange.Y = commonnessInfo.CaveCommonness; } + } + levelResources.Add((itemPrefab, commonnessInfo)); } - else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || + else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) || itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) { fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); @@ -2533,35 +2565,41 @@ namespace Barotrauma } } - //place some of the least common resources in the abyss + // Abyss Resources AbyssResources.Clear(); - int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, Difficulty / 100.0f); - - for (int i = 0; i < abyssClusterCount; i++) + var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f); + if (abyssResourcePrefabs.Any()) { - //use inverse commonness to select the abyss resources (the rarest ones are the most common in the abyss) - var selectedPrefab = ToolBox.SelectWeightedRandom( - levelResources.Select(it => it.itemPrefab).ToList(), - levelResources.Select(it => it.commonness <= 0.0f ? 0.0f : 1.0f / it.commonness).ToList(), - Rand.RandSync.ServerAndClient); - var location = allValidLocations.GetRandom(l => + int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, MathUtils.InverseLerp(LevelData.Biome.MinDifficulty, LevelData.Biome.AdjustedMaxDifficulty, Difficulty)); + for (int i = 0; i < abyssClusterCount; i++) { - if (l.Cell == null || l.Edge == null) { return false; } - if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } - l.InitializeResources(); - return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _); - }, randSync: Rand.RandSync.ServerAndClient); + var selectedPrefab = ToolBox.SelectWeightedRandom( + abyssResourcePrefabs.Select(r => r.itemPrefab).ToList(), + abyssResourcePrefabs.Select(r => r.commonnessInfo.AbyssCommonness).ToList(), + Rand.RandSync.ServerAndClient); + + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } + l.InitializeResources(); + return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _); + }, randSync: Rand.RandSync.ServerAndClient); + + if (location.Cell == null || location.Edge == null) { break; } + + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); + PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0); + var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); + abyssClusterLocation.Resources.AddRange(placedResources); + AbyssResources.Add(abyssClusterLocation); + + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); + allValidLocations.RemoveAt(locationIndex); + } + } - if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); - PlaceResources(selectedPrefab, clusterSize, location, out var abyssResources); - var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); - abyssClusterLocation.Resources.AddRange(abyssResources); - AbyssResources.Add(abyssClusterLocation); - var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); - allValidLocations.RemoveAt(locationIndex); - } PathPoints.Clear(); nextPathPointId = 0; @@ -2618,17 +2656,25 @@ namespace Barotrauma int itemCount = 0; Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() }; + var disabledPathPoints = new List(); // Create first cluster for each spawn point - foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) + foreach (var pathPoint in PathPoints) { if (itemCount >= GenerationParams.ItemCount) { break; } + if (!pathPoint.ShouldContainResources) { continue; } GenerateFirstCluster(pathPoint); + if (pathPoint.ClusterLocations.Count > 0) { continue; } + disabledPathPoints.Add(pathPoint.Id); + } + // Don't try to spawn more resource clusters for points for which the initial cluster could not be spawned + foreach (string pathPointId in disabledPathPoints) + { + if (PathPoints.FirstOrNull(p => p.Id == pathPointId) is PathPoint pathPoint) + { + PathPoints.RemoveAll(p => p.Id == pathPointId); + PathPoints.Add(pathPoint.WithResources(false)); + } } - - // Don't try to spawn more resource clusters for points - // for which the initial cluster could not be spawned - PathPoints.Where(p => p.ShouldContainResources && p.ClusterLocations.Count == 0) - .ForEach(p => p.ShouldContainResources = false); var excludedPathPointIds = new List(); while (itemCount < GenerationParams.ItemCount) @@ -2647,35 +2693,16 @@ namespace Barotrauma GenerateAdditionalCluster(pathPoint); } - // If none of the point set to contain resources can take more resources, - // but we still haven't reached the item count set in the generation parameters... - while (itemCount < GenerationParams.ItemCount) - { - // We need to start filling some of the path points previously set to not contain resources - Func availablePathPoints = p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None(); - if (PathPoints.None(availablePathPoints)) { break; } - var pathPoint = PathPoints.GetRandom(availablePathPoints, randSync: Rand.RandSync.ServerAndClient); - if (!GenerateFirstCluster(pathPoint)) - { - excludedPathPointIds.Add(pathPoint.Id); - continue; - } - while (pathPoint.NextClusterProbability > 0) - { - if (!GenerateAdditionalCluster(pathPoint)) { break; } - } - pathPoint.ShouldContainResources = pathPoint.ClusterLocations.Any(); - } - #if DEBUG - DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" + - " Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" + - " Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk"); + int spawnPointsContainingResources = PathPoints.Where(p => p.ClusterLocations.Any()).Count(); + string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count); + DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" + + $" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" + + $" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk"); if (AbyssResources.Count > 0) { - - DebugConsole.NewMessage("Abyss resources spawned: " + AbyssResources.Sum(a => a.Resources.Count) + "\n" + - " Total value: " + AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)) + " mk"); + DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" + + $" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk"); } #endif @@ -2842,7 +2869,7 @@ namespace Barotrauma { selectedPrefab = ToolBox.SelectWeightedRandom( levelResources.Select(it => it.itemPrefab).ToList(), - levelResources.Select(it => it.commonness).ToList(), + levelResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(), Rand.RandSync.ServerAndClient); selectedPrefab.Tags.ForEach(t => { @@ -2854,20 +2881,21 @@ namespace Barotrauma } else { - var filteredResources = levelResources.Where(it => - !pathPoint.ResourceIds.Contains(it.itemPrefab.Identifier) && - pathPoint.ResourceTags.Any() && it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); - selectedPrefab = ToolBox.SelectWeightedRandom( + var filteredResources = pathPoint.ResourceTags.None() ? levelResources : + levelResources.Where(it => it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); + selectedPrefab = ToolBox.SelectWeightedRandom( filteredResources.Select(it => it.itemPrefab).ToList(), - filteredResources.Select(it => it.commonness).ToList(), + filteredResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(), Rand.RandSync.ServerAndClient); } if (selectedPrefab == null) { return false; } // Create resources for the cluster - var commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonness; - var lerpAmount = MathUtils.InverseLerp(minCommonness, maxCommonness, commonness); + float commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonnessInfo.GetCommonness(pathPoint.TunnelType); + float lerpAmount = pathPoint.TunnelType != TunnelType.Cave ? + MathUtils.InverseLerp(commonnessRange.X, commonnessRange.Y, commonness) : + MathUtils.InverseLerp(caveCommonnessRange.X, caveCommonnessRange.Y, commonness); var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount); var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength); maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge); @@ -2898,58 +2926,92 @@ namespace Barotrauma edgeLength = 0.0f; if (location.Cell == null || location.Edge == null) { return 0; } edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + if (resourcePrefab == null) { return 0; } return (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X)); } } /// Used by clients to set the rotation for the resources - public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, out float rotation) + public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable targetCaves = null) { var allValidLocations = GetAllValidClusterLocations(); var placedResources = new List(); rotation = 0.0f; + if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?! + // Make sure not to pick a spot that already has other level resources for (int i = allValidLocations.Count - 1; i >= 0; i--) { - var location = allValidLocations[i]; - var locationHasResources = PathPoints.Any(p => - p.ClusterLocations.Any(c => - c.Equals(location) && - c.Resources.Any(r => r != null && !r.Removed && - (!(r.GetComponent() is Holdable h) || (h.Attachable && h.Attached))))); - if (locationHasResources) + if (HasResources(allValidLocations[i])) { allValidLocations.RemoveAt(i); } + + bool HasResources(ClusterLocation clusterLocation) + { + foreach (var p in PathPoints) + { + foreach (var c in p.ClusterLocations) + { + if (!c.Equals(clusterLocation)) { continue; } + foreach (var r in c.Resources) + { + if (r == null) { continue; } + if (r.Removed) { continue; } + if (!(r.GetComponent() is Holdable h) || (h.Attachable && h.Attached)) { return true; } + } + } + } + return false; + } } - var positionType = PositionType.MainPath; - if (PositionsOfInterest.Any(p => p.PositionType == PositionType.Cave)) + if (PositionsOfInterest.None(p => p.PositionType == positionType)) { - positionType = PositionType.Cave; - if (allValidLocations.Any(l => l.Edge.NextToCave)) + foreach (var validType in MineralMission.ValidPositionTypes) { - allValidLocations.RemoveAll(l => !l.Edge.NextToCave); + if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType)) + { + positionType = validType; + break; + } } } - else if (PositionsOfInterest.Any(p => p.PositionType == PositionType.SidePath)) + + try { - positionType = PositionType.SidePath; - if (allValidLocations.Any(l => l.Edge.NextToSidePath)) + RemoveInvalidLocations(positionType switch { - allValidLocations.RemoveAll(l => !l.Edge.NextToSidePath); - } + PositionType.MainPath => IsOnMainPath, + PositionType.SidePath => IsOnSidePath, + PositionType.Cave => IsInCave, + PositionType.AbyssCave => IsInAbyssCave, + _ => throw new NotImplementedException(), + }); + } + catch (NotImplementedException) + { + DebugConsole.ThrowError($"Unexpected PositionType (\"{positionType}\") for mineral mission resources: mineral spawning might not work as expected."); + } + + if (targetCaves != null && targetCaves.Any()) + { + // If resources are placed inside a cave, make sure all of them are placed inside the same one + allValidLocations.RemoveAll(l => targetCaves.None(c => c.Area.Contains(l.EdgeCenter))); } var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient); - var poiPos = poi.Position.ToVector2(); + Vector2 poiPos = poi.Position.ToVector2(); allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter) .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter))); - var maxResourceOverlap = 0.4f; + float maxResourceOverlap = 0.4f; var selectedLocation = allValidLocations.FirstOrDefault(l => Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength && + !l.Edge.OutsideLevel && requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X))); + + if (selectedLocation.Edge == null) { //couldn't find a long enough edge, find the largest one @@ -2968,9 +3030,18 @@ namespace Barotrauma throw new Exception("Failed to find a suitable level wall edge to place level resources on."); } PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); - var edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); + Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); return placedResources; + + static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath; + static bool IsOnSidePath(ClusterLocation location) => location.Edge.NextToSidePath; + static bool IsInCave(ClusterLocation location) => location.Edge.NextToCave; + bool IsInAbyssCave(ClusterLocation location) => location.EdgeCenter.Y < AbyssStart; + void RemoveInvalidLocations(Predicate match) + { + allValidLocations.RemoveAll(l => !match(l)); + } } private List GetAllValidClusterLocations() @@ -3041,15 +3112,19 @@ namespace Barotrauma { edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value; + if (!MathUtils.IsValid(edgeDir)) + { + edgeDir = Vector2.Zero; + } var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); - minResourceOverlap = Math.Max(minResourceOverlap, 0.0f); + minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap); var lerpAmounts = new float[resourceCount]; lerpAmounts[0] = 0.0f; var lerpAmount = 0.0f; for (int i = 1; i < resourceCount; i++) { var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient); - lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value; + lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value; lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); } var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient); @@ -3128,14 +3203,14 @@ namespace Barotrauma return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null, bool suppressWarning = false) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter); + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning); position = pos.ToVector2(); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null, bool suppressWarning = false) { if (!PositionsOfInterest.Any()) { @@ -3155,11 +3230,14 @@ namespace Barotrauma } if (!suitablePositions.Any()) { - string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + if (!suppressWarning) + { + string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg); #endif + } position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; return false; } @@ -3983,17 +4061,21 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } - float outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; - //don't try to compensate if the port is very far from the outpost's center of mass - if (Math.Abs(outpostDockingPortOffset) > 5000.0f) + float? outpostDockingPortOffset = null; + if (outpostPort != null) { - outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset, -5000.0f, 5000.0f); - 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.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; + //don't try to compensate if the port is very far from the outpost's center of mass + if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + { + outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); + 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.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } } - Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset - outpostDockingPortOffset, verticalMoveDir: 1); + Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); if (Type == LevelData.LevelType.Outpost) { spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index a6de99b67..340238aa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -57,6 +57,8 @@ namespace Barotrauma public readonly List EventHistory = new List(); public readonly List NonRepeatableEvents = new List(); + public bool EventsExhausted { get; set; } + public float CrushDepth { get @@ -130,6 +132,8 @@ namespace Barotrauma string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + + EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } @@ -238,7 +242,8 @@ namespace Barotrauma new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("size", XMLExtensions.PointToString(Size)), new XAttribute("generationparams", GenerationParams.Identifier), - new XAttribute("initialdepth", InitialDepth)); + new XAttribute("initialdepth", InitialDepth), + new XAttribute(nameof(EventsExhausted).ToLower(), EventsExhausted)); if (HasBeaconStation) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 3742ade58..498702fa1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -323,7 +323,7 @@ namespace Barotrauma set { caveCount = MathHelper.Clamp(value, 0, 100); } } - [Serialize(100, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(100, IsPropertySaveable.Yes, description: "The maximum number of level resources in the level."), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int ItemCount { get; @@ -344,7 +344,7 @@ namespace Barotrauma set; } - [Serialize("2,8", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " + + [Serialize("3,6", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " + "In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)] public Point ResourceClusterSizeRange { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 3b6aad96f..52bc59f55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -11,15 +11,7 @@ namespace Barotrauma { partial class LinkedSubmarinePrefab : MapEntityPrefab { - //public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - //Prefabs.Remove(this); - } + public override void Dispose() { } public readonly SubmarineInfo subInfo; @@ -289,6 +281,10 @@ namespace Barotrauma IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset); sub = Submarine.Load(info, false, parentRemap); sub.Info.SubmarineClass = Submarine.Info.SubmarineClass; + if (Submarine.Info.IsOutpost && Submarine.TeamID == CharacterTeamType.FriendlyNPC) + { + sub.TeamID = CharacterTeamType.FriendlyNPC; + } IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset); @@ -357,7 +353,9 @@ namespace Barotrauma float closestDistance = 0.0f; foreach (DockingPort port in DockingPort.List) { - if (port.Item.Submarine != sub || port.IsHorizontal != linkedPort.IsHorizontal) { continue; } + if (port.Item.Submarine != sub) { continue; } + if (port.IsHorizontal != linkedPort.IsHorizontal) { continue; } + if (port.ForceDockingDirection != DockingPort.DirectionType.None && port.ForceDockingDirection == linkedPort.ForceDockingDirection) { continue; } float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition); if (myPort == null || dist < closestDistance) { @@ -453,22 +451,22 @@ namespace Barotrauma saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)); - if (linkedTo.Any() || linkedToID.Any()) - { - var linkedPort = - linkedTo.FirstOrDefault(lt => (lt is Item item) && item.GetComponent() != null) ?? - FindEntityByID(linkedToID.First()) as MapEntity; - if (linkedPort != null) - { - saveElement.SetAttributeValue("linkedto", linkedPort.ID); - } - } } else { saveElement = new XElement("LinkedSubmarine"); sub.SaveToXElement(saveElement); } + if (linkedTo.Any() || linkedToID.Any()) + { + var linkedPort = + linkedTo.FirstOrDefault(lt => (lt is Item item) && item.GetComponent() != null) ?? + FindEntityByID(linkedToID.First()) as MapEntity; + if (linkedPort != null) + { + saveElement.SetAttributeValue("linkedto", linkedPort.ID); + } + } saveElement.SetAttributeValue("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID); saveElement.SetAttributeValue("originalmyport", originalMyPortID); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 08ac7d07d..cb1861ec7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -700,7 +700,7 @@ namespace Barotrauma #endif } - public MissionPrefab UnlockMissionByIdentifier(Identifier identifier) + public Mission UnlockMissionByIdentifier(Identifier identifier) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } @@ -721,17 +721,17 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); #endif - return missionPrefab; + return mission; } return null; } - public MissionPrefab UnlockMissionByTag(Identifier tag) + public Mission UnlockMissionByTag(Identifier tag) { var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); if (!matchingMissions.Any()) { - DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions not found."); + DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); } else { @@ -754,7 +754,7 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); #endif - return missionPrefab; + return mission; } else { @@ -1253,7 +1253,7 @@ namespace Barotrauma { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } - return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); + return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } public void Discover(bool checkTalents = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index b09b2400a..ff196b2d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -149,7 +149,7 @@ namespace Barotrauma Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ?? Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ?? Biome.Prefabs.First(); - connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.MaxDifficulty); + connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.AdjustedMaxDifficulty); connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty); Connections.Add(connection); connectionElements.Add(subElement); @@ -462,6 +462,13 @@ namespace Barotrauma } } + //make sure the connections are in the same order on the locations and the Connections list + //otherwise their order will change when loading the game (as they're added to the locations in the same order they're loaded) + foreach (var location in Locations) + { + location.Connections.Sort((c1, c2) => Connections.IndexOf(c1).CompareTo(Connections.IndexOf(c2))); + } + for (int i = Connections.Count - 1; i >= 0; i--) { i = Math.Min(i, Connections.Count - 1); @@ -562,7 +569,7 @@ namespace Barotrauma { if (connection.Locations.Any(l => l.IsGateBetweenBiomes)) { - connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty); + connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty); } else { @@ -591,7 +598,7 @@ namespace Barotrauma if (biome != null) { minDifficulty = biome.MinDifficulty; - maxDifficulty = biome.MaxDifficulty; + maxDifficulty = biome.AdjustedMaxDifficulty; float diff = 1 - settingsFactor; difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff); } @@ -944,15 +951,20 @@ namespace Barotrauma { foreach (Location location in Locations) { + location.LevelData.EventsExhausted = false; if (location.Discovered) { - if (furthestDiscoveredLocation == null || + if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; } } } + foreach (LocationConnection connection in Connections) + { + connection.LevelData.EventsExhausted = false; + } foreach (Location location in Locations) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 2e17abc77..6b481b5b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -340,7 +340,7 @@ namespace Barotrauma /// public virtual bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { - if (this is Item item && !upgrade.Prefab.UpgradeCategories.Any(category => category.CanBeApplied(item, upgrade.Prefab))) + if (!upgrade.Prefab.UpgradeCategories.Any(category => category.CanBeApplied(this, upgrade.Prefab))) { return false; } @@ -361,16 +361,6 @@ namespace Barotrauma Upgrades.Add(upgrade); } - // not used anymore -#if SERVER - // if (createNetworkEvent) - // { - // if (this is IServerSerializable serializable) - // { - // GameMain.Server.CreateEntityEvent(serializable, new object[] { NetEntityEvent.Type.Upgrade, upgrade }); - // } - // } -#endif return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 258e7262f..bdec8d1b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -280,5 +280,29 @@ namespace Barotrauma return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier) || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } + + protected void LoadDescription(ContentXElement element) + { + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); + + string originalDescription = Description.Value; + if (descriptionIdentifier != Identifier.Empty) + { + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); + } + else if (nameIdentifier == Identifier.Empty) + { + Description = TextManager.Get($"EntityDescription.{Identifier}"); + } + else + { + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); + } + if (!originalDescription.IsNullOrEmpty()) + { + Description = Description.Fallback(originalDescription); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 48cce8bfc..965965805 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -19,7 +19,7 @@ namespace Barotrauma public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file)).ToImmutableArray(); + Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file, Identifier)).ToImmutableArray(); } public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 4c65efaa3..6e9b95be9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -203,7 +203,7 @@ namespace Barotrauma } else { - newCollection.Add(new HumanPrefab(npcElement, file)); + newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from)); } } humanPrefabCollections.Add(newCollection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index a0e5ceed6..30217e2a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1426,7 +1426,7 @@ namespace Barotrauma static bool ShouldRemoveLinkedEntity(MapEntity e, bool doorInUse, PlacedModule module) { - if (e is Item it && it.GetComponent() != null) + if (e is Item it && it.IsLadder) { if (module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Top) || module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Bottom)) { @@ -1568,7 +1568,7 @@ namespace Barotrauma foreach (HumanPrefab humanPrefab in humanPrefabs) { if (humanPrefab is null) { continue; } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); + var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { killedCharacters.Add(humanPrefab); @@ -1582,7 +1582,7 @@ namespace Barotrauma { for (int tries = 0; tries < 100; tries++) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); + var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient); if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { selectedCharacters.Add((killedCharacter, characterInfo)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs deleted file mode 100644 index 1fb8afbae..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - class RoundEndCinematic - { - public bool Running - { - get; - private set; - } - - public Camera AssignedCamera; - - private float duration; - - private CoroutineHandle updateCoroutine; - - public RoundEndCinematic(Submarine submarine, Camera cam, float duration = 10.0f) - : this(new List() { submarine }, cam, duration) - { - - } - - public RoundEndCinematic(List submarines, Camera cam, float duration) - { - if (!submarines.Any(s => s != null)) return; - - this.duration = duration; - AssignedCamera = cam; - - Running = true; - updateCoroutine = CoroutineManager.StartCoroutine(Update(submarines, cam)); - } - - public void Stop() - { - CoroutineManager.StopCoroutines(updateCoroutine); - Running = false; -#if CLIENT - GUI.ScreenOverlayColor = Color.TransparentBlack; -#endif - } - - private IEnumerable Update(List subs, Camera cam) - { - if (!subs.Any()) yield return CoroutineStatus.Success; - -#if CLIENT - Character.Controlled = null; - GameMain.LightManager.LosEnabled = false; -#endif - cam.TargetPos = Vector2.Zero; - - 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; - Vector2 initialCameraPos = cam.Position; - - while (timer < duration) - { - if (Screen.Selected != GameMain.GameScreen) - { - yield return new WaitForSeconds(0.1f); - -#if CLIENT - GUI.ScreenOverlayColor = Color.TransparentBlack; -#endif - - Running = false; - yield return CoroutineStatus.Success; - } - - Vector2 minPos = new Vector2( - subs.Min(s => s.WorldPosition.X - s.Borders.Width / 2), - subs.Min(s => s.WorldPosition.Y - s.Borders.Height / 2)); - Vector2 maxPos = new Vector2( - subs.Min(s => s.WorldPosition.X + s.Borders.Width / 2), - subs.Min(s => s.WorldPosition.Y + s.Borders.Height / 2)); - Vector2 cameraPos = new Vector2( - MathHelper.SmoothStep(minPos.X, maxPos.X, timer / duration), - (minPos.Y + maxPos.Y) / 2.0f); - cam.Translate(cameraPos - cam.Position); - - foreach (Submarine sub in subs) - { - sub.PhysicsBody?.ResetDynamics(); - } - -#if CLIENT - cam.Zoom = MathHelper.SmoothStep(initialZoom, 0.5f, timer / duration); - if (timer / duration > 0.9f) - { - GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / duration) - 0.9f) * 10.0f); - } -#endif - timer += CoroutineManager.UnscaledDeltaTime; - - yield return CoroutineStatus.Running; - } - - Running = false; - - yield return new WaitForSeconds(0.1f); - -#if CLIENT - GUI.ScreenOverlayColor = Color.TransparentBlack; -#endif - - yield return CoroutineStatus.Success; - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index b973831de..600a2b731 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -168,6 +168,17 @@ namespace Barotrauma public ImmutableHashSet Tags => Prefab.Tags; +#if DEBUG + [Editable, Serialize("", IsPropertySaveable.Yes)] +#else + [Serialize("", IsPropertySaveable.Yes)] +#endif + public string SpecialTag + { + get; + set; + } + protected Color spriteColor; [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor @@ -574,6 +585,11 @@ namespace Barotrauma int xsections = 1, ysections = 1; int width = rect.Width, height = rect.Height; + WallSection[] prevSections = null; + if (Sections != null) + { + prevSections = Sections.ToArray(); + } if (!HasBody) { if (FlippedX && IsHorizontal) @@ -657,6 +673,14 @@ namespace Barotrauma } } } + + if (prevSections != null && Sections.Length == prevSections.Length) + { + for (int i = 0; i < Sections.Length; i++) + { + Sections[i].damage = prevSections[i].damage; + } + } } private Rectangle GenerateMergedRect(List mergedSections) @@ -829,27 +853,33 @@ namespace Barotrauma public WallSection GetSection(int sectionIndex) { - if (sectionIndex < 0 || sectionIndex >= Sections.Length) return null; - + if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return null; } return Sections[sectionIndex]; } public bool SectionBodyDisabled(int sectionIndex) { - if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false; - + if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; } return (Sections[sectionIndex].damage >= MaxHealth); } + public bool AllSectionBodiesDisabled() + { + for (int i = 0; i < Sections.Length; i++) + { + if (Sections[i].damage < MaxHealth) { return false; } + } + return true; + } + /// /// Sections that are leaking have a gap placed on them /// public bool SectionIsLeaking(int sectionIndex) { - if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false; - - return (Sections[sectionIndex].damage >= MaxHealth * LeakThreshold); + if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; } + return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold; } public int SectionLength(int sectionIndex) @@ -1139,21 +1169,22 @@ namespace Barotrauma gapRect.Height += 20; bool horizontalGap = !IsHorizontal; + bool diagonalGap = false; if (Prefab.BodyRotation != 0.0f) { //rotation within a 90 deg sector (e.g. 100 -> 10, 190 -> 10, -10 -> 80) float sectorizedRotation = MathUtils.WrapAngleTwoPi(BodyRotation) % MathHelper.PiOver2; //diagonal if 30 < angle < 60 - bool diagonal = sectorizedRotation > MathHelper.Pi / 6 && sectorizedRotation < MathHelper.Pi / 3; + diagonalGap = sectorizedRotation > MathHelper.Pi / 6 && sectorizedRotation < MathHelper.Pi / 3; //gaps on the lower half of a diagonal wall are horizontal, ones on the upper half are vertical - if (diagonal) + if (diagonalGap) { horizontalGap = gapRect.Y - gapRect.Height / 2 < Position.Y; if (FlippedY) { horizontalGap = !horizontalGap; } } } - Sections[sectionIndex].gap = new Gap(gapRect, horizontalGap, Submarine); + Sections[sectionIndex].gap = new Gap(gapRect, horizontalGap, Submarine, isDiagonal: diagonalGap); //free the ID, because if we give gaps IDs we have to make sure they always match between the clients and the server and //that clients create them in the correct order along with every other entity created/removed during the round diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 30c8ae40b..3dd5eea35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -142,8 +142,6 @@ namespace Barotrauma //only used if the item doesn't have a name/description defined in the currently selected language Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", ""); - Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - Name = TextManager.Get(nameIdentifier.IsEmpty ? $"EntityName.{Identifier}" : $"EntityName.{nameIdentifier}", @@ -271,21 +269,7 @@ namespace Barotrauma tags.Add("wall".ToIdentifier()); } - if (Description.IsNullOrEmpty()) - { - if (!descriptionIdentifier.IsEmpty) - { - Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); - } - else if (nameIdentifier.IsEmpty) - { - Description = TextManager.Get($"EntityDescription.{Identifier}"); - } - else - { - Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); - } - } + LoadDescription(element); //backwards compatibility if (element.GetAttribute("size") == null) @@ -334,12 +318,6 @@ namespace Barotrauma throw new NotImplementedException(); } - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 4e6e22f7f..5ae404712 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -187,7 +187,6 @@ namespace Barotrauma if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; } realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value); } - realWorldCrushDepth *= Info.GetRealWorldCrushDepthMultiplier(); } return realWorldCrushDepth.Value; } @@ -452,10 +451,27 @@ namespace Barotrauma verticalMoveDir = Math.Sign(verticalMoveDir); //do a raycast towards the top/bottom of the level depending on direction Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0); - if (PickBody(ConvertUnits.ToSimUnits(spawnPos), ConvertUnits.ToSimUnits(potentialPos), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + + //3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub) + for (int x = -1; x <= 1; x++) { - //if the raycast hit a wall, attempt to place the spawnpos there - potentialPos.Y = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) - 10; + Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x; + if (PickBody( + ConvertUnits.ToSimUnits(spawnPos + xOffset), + ConvertUnits.ToSimUnits(potentialPos + xOffset), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + { + int offsetFromWall = 10 * -verticalMoveDir; + //if the raycast hit a wall, attempt to place the spawnpos there + if (verticalMoveDir > 0) + { + potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); + } + else + { + potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); + } + } } //step away from the top/bottom of the level, or from whatever wall the raycast hit, @@ -986,14 +1002,6 @@ namespace Barotrauma subBody.Body.ResetDynamics(); subBody.Body.Enabled = false; - foreach (MapEntity e in MapEntity.mapEntityList) - { - if (e.Submarine == this) - { - Spawner.AddEntityToRemoveQueue(e); - } - } - foreach (Character c in Character.CharacterList) { if (c.Submarine == this) @@ -1226,7 +1234,7 @@ namespace Barotrauma public List<(ItemContainer container, int freeSlots)> GetCargoContainers() { List<(ItemContainer container, int freeSlots)> containers = new List<(ItemContainer container, int freeSlots)>(); - var connectedSubs = GetConnectedSubs(); + var connectedSubs = GetConnectedSubs().Where(sub => sub.Info?.Type == Info.Type); foreach (Item item in Item.ItemList.ToList()) { if (!connectedSubs.Contains(item.Submarine)) { continue; } @@ -1540,6 +1548,7 @@ namespace Barotrauma element.Add(new XAttribute("description", Info.Description ?? "")); element.Add(new XAttribute("checkval", Rand.Int(int.MaxValue))); element.Add(new XAttribute("price", Info.Price)); + element.Add(new XAttribute("tier", Info.Tier)); element.Add(new XAttribute("initialsuppliesspawned", Info.InitialSuppliesSpawned)); element.Add(new XAttribute("noitems", Info.NoItems)); element.Add(new XAttribute("lowfuel", !CheckFuel())); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 673a6218b..b9af39add 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -1,8 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Collision; using FarseerPhysics.Common; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; @@ -345,14 +343,12 @@ namespace Barotrauma Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height)))); } - //hard limit for how far outside the level the sub can go - float maxDist = 200000.0f; - //the force of the current starts to increase exponentially after this point - float exponentialForceIncreaseDist = 150000.0f; - float distance = Position.X < 0 ? Math.Abs(Position.X) : Position.X - Level.Loaded.Size.X; + float distance = Position.X < -Level.OutsideBoundsCurrentMargin ? + Math.Abs(Position.X + Level.OutsideBoundsCurrentMargin) : + Position.X - (Level.Loaded.Size.X + Level.OutsideBoundsCurrentMargin); if (distance > 0) { - if (distance > maxDist) + if (distance > Level.OutsideBoundsCurrentHardLimit) { if (Position.X < 0) { @@ -363,9 +359,9 @@ namespace Barotrauma Body.LinearVelocity = new Vector2(Math.Min(0, Body.LinearVelocity.X), Body.LinearVelocity.Y); } } - if (distance > exponentialForceIncreaseDist) + if (distance > Level.OutsideBoundsCurrentMarginExponential) { - distance += (float)Math.Pow((distance - exponentialForceIncreaseDist) * 0.01f, 2.0f); + distance += (float)Math.Pow((distance - Level.OutsideBoundsCurrentMarginExponential) * 0.01f, 2.0f); } float force = distance * 0.5f; totalForce += (Position.X < 0 ? Vector2.UnitX : -Vector2.UnitX) * force; @@ -451,11 +447,13 @@ namespace Barotrauma private Vector2 CalculateBuoyancy() { + if (Submarine.LockY) { return Vector2.Zero; } + float waterVolume = 0.0f; float volume = 0.0f; foreach (Hull hull in Hull.HullList) { - if (hull.Submarine != submarine) continue; + if (hull.Submarine != submarine) { continue; } waterVolume += hull.WaterVolume; volume += hull.Volume; @@ -509,7 +507,6 @@ namespace Barotrauma if (wall.Submarine != submarine) { continue; } float wallCrushDepth = wall.CrushDepth; - if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; } float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; if (pastCrushDepth > 0) { @@ -591,9 +588,13 @@ namespace Barotrauma newHull = Hull.FindHull(targetPos, null); } - var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); - Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); - if (adjacentGap == null) { return true; } + //if all the bodies of a wall have been disabled, we don't need to care about gaps (can always pass through) + if (!(contact.FixtureA.UserData is Structure wall) || !wall.AllSectionBodiesDisabled()) + { + var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); + Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); + if (adjacentGap == null) { return true; } + } if (newHull != null) { @@ -898,13 +899,15 @@ namespace Barotrauma } bool holdingOntoSomething = false; - if (c.SelectedConstruction != null) + if (c.SelectedSecondaryItem != null) { - holdingOntoSomething = - c.SelectedConstruction.GetComponent() != null || - (c.SelectedConstruction.GetComponent()?.LimbPositions.Any() ?? false); + holdingOntoSomething = c.SelectedSecondaryItem.IsLadder || + (c.SelectedSecondaryItem.GetComponent()?.LimbPositions.Any() ?? false); + } + if (!holdingOntoSomething && c.SelectedItem != null) + { + holdingOntoSomething = c.SelectedItem.GetComponent()?.LimbPositions.Any() ?? false; } - if (!holdingOntoSomething) { c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 898f27b77..426941fe8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -24,7 +24,7 @@ namespace Barotrauma } public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine, Ruin } - public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver } + public enum SubmarineClass { Undefined, Scout, Attack, Transport } partial class SubmarineInfo : IDisposable { @@ -49,6 +49,12 @@ namespace Barotrauma } public CrewExperienceLevel RecommendedCrewExperience; + public int Tier + { + get; + set; + } + /// /// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match /// @@ -305,6 +311,7 @@ namespace Barotrauma RecommendedCrewExperience = original.RecommendedCrewExperience; RecommendedCrewSizeMin = original.RecommendedCrewSizeMin; RecommendedCrewSizeMax = original.RecommendedCrewSizeMax; + Tier = original.Tier; IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; if (original.OutpostModuleInfo != null) @@ -386,6 +393,7 @@ namespace Barotrauma { Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience); } + Tier = SubmarineElement.GetAttributeInt("tier", GetDefaultTier(Price)); if (SubmarineElement?.Attribute("type") != null) { @@ -407,7 +415,13 @@ namespace Barotrauma { if (SubmarineElement?.Attribute("class") != null) { - if (Enum.TryParse(SubmarineElement.GetAttributeString("class", "Undefined"), out SubmarineClass submarineClass)) + string classStr = SubmarineElement.GetAttributeString("class", "Undefined"); + if (classStr == "DeepDiver") + { + //backwards compatibility + SubmarineClass = SubmarineClass.Scout; + } + else if (Enum.TryParse(classStr, out SubmarineClass submarineClass)) { SubmarineClass = submarineClass; } @@ -538,25 +552,9 @@ namespace Barotrauma { realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; } - realWorldCrushDepth *= GetRealWorldCrushDepthMultiplier(); return realWorldCrushDepth; } - /// - /// Based on - /// - public float GetRealWorldCrushDepthMultiplier() - { - if (SubmarineClass == SubmarineClass.DeepDiver) - { - return 1.2f; - } - else - { - return 1.0f; - } - } - //saving/loading ---------------------------------------------------- public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { @@ -691,7 +689,7 @@ namespace Barotrauma System.IO.Stream stream; try { - stream = SaveUtil.DecompressFiletoStream(file); + stream = SaveUtil.DecompressFileToStream(file); } catch (System.IO.FileNotFoundException e) { @@ -748,5 +746,7 @@ namespace Barotrauma return doc; } + + public static int GetDefaultTier(int price) => price > 20000 ? 3 : price > 10000 ? 2 : 1; } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 6972ac705..1a99f675a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -19,7 +19,7 @@ namespace Barotrauma public static bool ShowWayPoints = true, ShowSpawnPoints = true; - public const float LadderWaypointInterval = 70.0f; + public const float LadderWaypointInterval = 55.0f; protected SpawnType spawnType; private string[] idCardTags; @@ -560,21 +560,22 @@ namespace Barotrauma stairPoints.ForEach(wp => wp.FindStairs()); } + // Ladders foreach (Item item in Item.ItemList) { var ladders = item.GetComponent(); if (ladders == null) { continue; } Vector2 bottomPoint = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10); - List ladderPoints = new List + List<(WayPoint wp, bool connectHullPoints)> ladderPoints = new List<(WayPoint, bool)> { - new WayPoint(bottomPoint, SpawnType.Path, submarine), + (new WayPoint(bottomPoint, SpawnType.Path, submarine), true) }; List ignoredBodies = new List(); // Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either. // Start point is where the bots normally grasp the ladder when they stand on ground. - WayPoint lowestPoint = ladderPoints[0]; + WayPoint lowestPoint = ladderPoints[0].wp; WayPoint prevPoint = lowestPoint; Vector2 prevPos = prevPoint.SimPosition; Body ground = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies, @@ -589,7 +590,7 @@ namespace Barotrauma if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null) { startPoint = new WayPoint(nextPos, SpawnType.Path, submarine); - ladderPoints.Add(startPoint); + ladderPoints.Add((startPoint, true)); if (lowestPoint != null) { startPoint.ConnectTo(lowestPoint); @@ -613,18 +614,13 @@ namespace Barotrauma } else { - //no door, check for walls + //no door, check for platforms/walls pickedBody = Submarine.PickBody( ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false, (Fixture f) => f.Body.UserData is Structure); } - if (pickedBody == null) - { - prevPos = Submarine.LastPickedPosition; - continue; - } - else + if (pickedBody != null) { ignoredBodies.Add(pickedBody); } @@ -632,19 +628,29 @@ namespace Barotrauma if (pickedDoor != null) { WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine); - ladderPoints.Add(newPoint); + ladderPoints.Add((newPoint, true)); newPoint.ConnectedGap = pickedDoor.LinkedGap; + // TODO: Prevent the waypoint below being too close to the door newPoint.ConnectTo(prevPoint); prevPoint = newPoint; prevPos = new Vector2(prevPos.X, ConvertUnits.ToSimUnits(pickedDoor.Item.Position.Y - pickedDoor.Item.Rect.Height)); + // Adjust y to prevent waypoints clamping up together + y = Math.Max(pickedDoor.Item.Position.Y, y); } else { - WayPoint newPoint = new WayPoint(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor, SpawnType.Path, submarine); - ladderPoints.Add(newPoint); + Vector2 pos = pickedBody == null ? new Vector2(startPoint.Position.X, y) : + ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor; + WayPoint newPoint = new WayPoint(pos, SpawnType.Path, submarine); + ladderPoints.Add((newPoint, pickedBody != null)); newPoint.ConnectTo(prevPoint); prevPoint = newPoint; prevPos = ConvertUnits.ToSimUnits(newPoint.Position); + if (pickedBody != null) + { + // Adjust y to prevent waypoints clamping up together + y = Math.Max(newPoint.Position.Y, y); + } } } @@ -652,33 +658,30 @@ namespace Barotrauma if (prevPoint.rect.Y < item.Rect.Y - 40) { WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine); - ladderPoints.Add(wayPoint); + ladderPoints.Add((wayPoint, true)); wayPoint.ConnectTo(prevPoint); } // Connect ladder waypoints to hull points at the right and left side - foreach (WayPoint ladderPoint in ladderPoints) + var ladderWaypoints = ladderPoints.Select(lp => lp.wp); + foreach (var ladderPoint in ladderPoints) { - ladderPoint.Ladders = ladders; - bool isHatch = ladderPoint.ConnectedGap != null && !ladderPoint.ConnectedGap.IsRoomToRoom; + var wp = ladderPoint.wp; + wp.Ladders = ladders; + if (!ladderPoint.connectHullPoints) { continue; } + bool isHatch = wp.ConnectedGap != null && !wp.ConnectedGap.IsRoomToRoom; for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = null; - if (isHatch) - { - closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderPoints); - } - else - { - closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 100), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints); - } + WayPoint closest = isHatch ? + wp.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), wp.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderWaypoints) : + wp.FindClosest(dir, horizontalSearch: true, new Vector2(150, 100), wp.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderWaypoints); if (closest == null) { continue; } - ladderPoint.ConnectTo(closest); + wp.ConnectTo(closest); } } } - // Another pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders) + // Another ladder pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders) foreach (Item item in Item.ItemList) { var ladders = item.GetComponent(); @@ -1035,12 +1038,10 @@ namespace Barotrauma w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); - string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant(); - if (!string.IsNullOrWhiteSpace(jobIdentifier)) + Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty); + if (!jobIdentifier.IsEmpty) { - w.AssignedJob = - JobPrefab.Get(jobIdentifier) ?? - JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase)); + w.AssignedJob = JobPrefab.Get(jobIdentifier); } w.linkedToID = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs new file mode 100644 index 000000000..4885dd27a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs @@ -0,0 +1,168 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Networking; +using Lidgren.Network; + +namespace Barotrauma +{ + interface IWritableBitField + { + public void WriteBoolean(bool b); + public void WriteInteger(int value, int min, int max); + public void WriteFloat(float value, float min, float max, int numberOfBits); + + public void WriteToMessage(IWriteMessage msg); + } + + interface IReadableBitField + { + public bool ReadBoolean(); + public int ReadInteger(int min, int max); + public float ReadFloat(float min, float max, int numberOfBits); + } + + sealed class WriteOnlyBitField : IWritableBitField, IDisposable + { + private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker + private readonly List Buffer = new List(); + private int index; + private bool disposed; + + public void WriteBoolean(bool b) + { + ThrowIfDisposed(); + + int arrayIndex = (int)Math.Floor(index / (float)AmountOfBoolsInByte); + if (arrayIndex >= Buffer.Count) { Buffer.Add(0); } + + int bitIndex = index % AmountOfBoolsInByte; + Buffer[arrayIndex] |= (byte)(b ? 1u << bitIndex : 0); + index++; + } + + public void WriteInteger(int value, int min, int max) + { + ThrowIfDisposed(); + + uint range = (uint)(max - min); + int numberOfBits = NetUtility.BitsToHoldUInt(range); + + uint writeValue = (uint)(value - min); + + for (int i = 0; i < numberOfBits; i++) + { + WriteBoolean((writeValue & (1u << i)) != 0); + } + } + + public void WriteFloat(float value, float min, float max, int numberOfBits) + { + ThrowIfDisposed(); + + float range = max - min; + float unit = (value - min) / range; + uint maxVal = (1u << numberOfBits) - 1; + + uint writeValue = (uint)(maxVal * unit); + for (int i = 0; i < numberOfBits; i++) + { + WriteBoolean((writeValue & (1u << i)) != 0); + } + } + + public void WriteToMessage(IWriteMessage msg) + { + ThrowIfDisposed(); + + if (Buffer.Count == 0) { Buffer.Add(0); } + + Buffer[^1] |= 1 << AmountOfBoolsInByte; // mark the last byte so we know when to stop reading + + foreach (byte b in Buffer) + { + msg.WriteByte(b); + } + + Dispose(); + } + + public void Dispose() + { + disposed = true; + } + + private void ThrowIfDisposed() + { + if (disposed) { throw new ObjectDisposedException(nameof(WriteOnlyBitField)); } + } + } + + sealed class ReadOnlyBitField : IReadableBitField + { + private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker + private readonly ImmutableArray buffer; + private int index; + + public ReadOnlyBitField(IReadMessage inc) + { + List bytes = new List(); + byte currentByte; + int reads = 0; + do + { + currentByte = inc.ReadByte(); + bytes.Add(currentByte); + + reads++; + if (reads > 100) + { + throw new Exception($"Failed to find the end of the bit field after 100 reads. Terminating to prevent the game from freezing."); + } + } + while (!IsBitSet(currentByte, AmountOfBoolsInByte)); + + buffer = bytes.ToImmutableArray(); + } + + public bool ReadBoolean() + { + int arrayIndex = (int)MathF.Floor(index / (float)AmountOfBoolsInByte); + int bitIndex = index % AmountOfBoolsInByte; + index++; + return IsBitSet(buffer[arrayIndex], bitIndex); + } + + public int ReadInteger(int min, int max) + { + uint range = (uint)(max - min); + int numberOfBits = NetUtility.BitsToHoldUInt(range); + + uint value = 0; + for (int i = 0; i < numberOfBits; i++) + { + value |= ReadBoolean() ? 1u << i : 0u; + } + + return (int)(min + value); + } + + public float ReadFloat(float min, float max, int numberOfBits) + { + int maxInt = (1 << numberOfBits) - 1; + + uint value = 0; + for (int i = 0; i < numberOfBits; i++) + { + value |= ReadBoolean() ? 1u << i : 0u; + } + + float range = max - min; + return min + range * value / maxInt; + } + + private static bool IsBitSet(byte b, int bitIndex) => (b & (1u << bitIndex)) != 0; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index 4bc42b15c..e08ff3ca1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -1,37 +1,35 @@ -using Barotrauma.Steam; +#nullable enable using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Networking { + #warning TODO: turn this into INetSerializableStruct partial class BannedPlayer { - public string Name; - public string EndPoint; public bool IsRangeBan; - public UInt64 SteamID; - public string Reason; - public DateTime? ExpirationTime; - public UInt16 UniqueIdentifier; + public readonly string Name; + public readonly Either AddressOrAccountId; - private void ParseEndPointAsSteamId() - { - ulong endPointAsSteamId = SteamManager.SteamIDStringToUInt64(EndPoint); - if (endPointAsSteamId != 0 && SteamID == 0) { SteamID = endPointAsSteamId; } - } + public readonly string Reason; + public DateTime? ExpirationTime; + public readonly UInt32 UniqueIdentifier; } partial class BanList { private readonly List bannedPlayers; + + public IReadOnlyList BannedPlayers => bannedPlayers; + public IEnumerable BannedNames { get { return bannedPlayers.Select(bp => bp.Name); } } - public IEnumerable BannedEndPoints + public IEnumerable> BannedAddresses { - get { return bannedPlayers.Select(bp => bp.EndPoint).Where(endPoint => !string.IsNullOrEmpty(endPoint)); } + get { return bannedPlayers.Select(bp => bp.AddressOrAccountId); } } partial void InitProjectSpecific(); @@ -42,19 +40,5 @@ namespace Barotrauma.Networking bannedPlayers = new List(); InitProjectSpecific(); } - - public static string ToRange(string ip) - { - if (SteamManager.SteamIDStringToUInt64(ip) != 0) { return ip; } - for (int i = ip.Length - 1; i > 0; i--) - { - if (ip[i] == '.') - { - ip = ip.Substring(0, i) + ".x"; - break; - } - } - return ip; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 144a567ca..8cb93bbb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -98,7 +98,7 @@ namespace Barotrauma.Networking Task readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token); if (readTask is null) { return -1; } - TimeSpan timeOut = TimeSpan.FromMilliseconds(100); + int timeOutMilliseconds = 100; for (int i = 0; i < 150; i++) { if (shutDown) @@ -106,12 +106,9 @@ namespace Barotrauma.Networking readCancellationToken?.Cancel(); return -1; } - - // BUG workaround for crash when closing the server under .NET 6.0, not sure if this is the proper way to fix it but it prevents it from crashing the client. - Markus -#if NET6_0 try { - if (readTask.IsCompleted || readTask.Wait(100, readCancellationToken.Token)) + if (readTask.IsCompleted || readTask.Wait(timeOutMilliseconds, readCancellationToken.Token)) { break; } @@ -120,12 +117,6 @@ namespace Barotrauma.Networking { return -1; } -#else - if (readTask.IsCompleted || readTask.Wait(timeOut)) - { - break; - } -#endif } if (readTask.Status != TaskStatus.RanToCompletion) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index f6f1802ae..93a7b09eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -11,16 +11,15 @@ namespace Barotrauma.Networking public string Name; public Identifier PreferredJob; public CharacterTeamType PreferredTeam; - public UInt16 NameID; - public UInt64 SteamID; - public byte ID; - public UInt16 CharacterID; + public UInt16 NameId; + public AccountInfo AccountInfo; + public byte SessionId; + public UInt16 CharacterId; public float Karma; public bool Muted; public bool InGame; public bool HasPermissions; public bool IsOwner; - public bool AllowKicking; public bool IsDownloading; } @@ -28,10 +27,23 @@ namespace Barotrauma.Networking { public const int MaxNameLength = 32; - public string Name; public UInt16 NameID; - public byte ID; - public UInt64 SteamID; - public UInt64 OwnerSteamID; + public string Name; public UInt16 NameId; + + /// + /// An ID for this client for the current session. + /// THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG-TERM. + /// IT CANNOT BE USED TO IDENTIFY PLAYERS ACROSS SESSIONS. + /// + public readonly byte SessionId; + + public AccountInfo AccountInfo; + + /// + /// The ID of the account used to authenticate this session. + /// This value can be used as a persistent value to identify + /// players in the banlist and campaign saves. + /// + public Option AccountId => AccountInfo.AccountId; public LanguageIdentifier Language; @@ -90,14 +102,14 @@ namespace Barotrauma.Networking public UInt16 CharacterID; - private Vector2 spectate_position; + private Vector2 spectatePos; public Vector2? SpectatePos { get { if (character == null || character.IsDead) { - return spectate_position; + return spectatePos; } else { @@ -107,7 +119,7 @@ namespace Barotrauma.Networking set { - spectate_position = value.Value; + spectatePos = value.Value; } } @@ -164,8 +176,6 @@ namespace Barotrauma.Networking } public bool HasSpawned; //has the client spawned as a character during the current round - private readonly List kickVoters; - public HashSet GivenAchievements = new HashSet(); public ClientPermissions Permissions = ClientPermissions.None; @@ -173,25 +183,12 @@ namespace Barotrauma.Networking private readonly object[] votes; - public int KickVoteCount - { - get { return kickVoters.Count; } - } - - /*public Client(NetPeer server, string name, byte ID) - : this(name, ID) - { - - }*/ - partial void InitProjSpecific(); partial void DisposeProjSpecific(); - public Client(string name, byte ID) + public Client(string name, byte sessionId) { this.Name = name; - this.ID = ID; - - kickVoters = new List(); + this.SessionId = sessionId; votes = new object[Enum.GetNames(typeof(VoteType)).Length]; @@ -207,57 +204,21 @@ namespace Barotrauma.Networking { votes[(int)voteType] = value; } - - public void ResetVotes() - { - for (int i = 0; i < votes.Length; i++) - { - votes[i] = null; - } - - kickVoters.Clear(); - } - - public void AddKickVote(Client voter) - { - if (voter != null && !kickVoters.Contains(voter)) { kickVoters.Add(voter); } - } - - - public void RemoveKickVote(Client voter) - { - kickVoters.Remove(voter); - } - - public bool HasKickVoteFrom(Client voter) - { - return kickVoters.Contains(voter); - } - - public bool HasKickVoteFromID(int id) - { - return kickVoters.Any(k => k.ID == id); - } - - - public static void UpdateKickVotes(List connectedClients) - { - foreach (Client client in connectedClients) - { - client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter)); - } - } + + public bool SessionOrAccountIdMatches(string userId) + => (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId) + || (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId); public void WritePermissions(IWriteMessage msg) { - msg.Write(ID); + msg.WriteByte(SessionId); msg.WriteRangedInteger((int)Permissions, 0, (int)ClientPermissions.All); if (HasPermission(ClientPermissions.ConsoleCommands)) { - msg.Write((UInt16)PermittedConsoleCommands.Count); + msg.WriteUInt16((UInt16)PermittedConsoleCommands.Count); foreach (DebugConsole.Command command in PermittedConsoleCommands) { - msg.Write(command.names[0]); + msg.WriteString(command.names[0]); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 051c2b1f7..6e5300c4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -32,8 +32,8 @@ namespace Barotrauma.Networking class PermissionPreset { - public static List List = new List(); - + public static readonly List List = new List(); + public readonly LocalizedString Name; public readonly LocalizedString Description; public readonly ClientPermissions Permissions; @@ -87,9 +87,11 @@ namespace Barotrauma.Networking } } - public bool MatchesPermissions(ClientPermissions permissions, HashSet permittedConsoleCommands) + public bool MatchesPermissions(ClientPermissions permissions, ISet permittedConsoleCommands) { - return permissions == this.Permissions && PermittedCommands.SequenceEqual(permittedConsoleCommands); + return permissions == Permissions + && PermittedCommands.All(permittedConsoleCommands.Contains) + && permittedConsoleCommands.All(PermittedCommands.Contains); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index 950a58719..b8e4460b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.Serialization; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -35,7 +35,7 @@ namespace Barotrauma /// Using the attribute on the struct will make all fields and properties serialized /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)] - public class NetworkSerialize : Attribute + public sealed class NetworkSerialize : Attribute { public int MaxValueInt = int.MaxValue; public int MinValueInt = int.MinValue; @@ -56,21 +56,37 @@ namespace Barotrauma /// /// Static class that contains serialize and deserialize functions for different types used in /// - public static class NetSerializableProperties + [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod")] + static class NetSerializableProperties { - public readonly struct ReadWriteBehavior + public interface IReadWriteBehavior { - public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute); + public delegate object? ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField); - public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg); + public delegate void WriteDelegate(object? obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField); - public readonly ReadDelegate ReadAction; - public readonly WriteDelegate WriteAction; + public ReadDelegate ReadAction { get; } + public WriteDelegate WriteAction { get; } + } + + public readonly struct ReadWriteBehavior : IReadWriteBehavior + { + public delegate T ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField); + + public delegate void WriteDelegate(T obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField); + + public IReadWriteBehavior.ReadDelegate ReadAction { get; } + public IReadWriteBehavior.WriteDelegate WriteAction { get; } + + public ReadDelegate ReadActionDirect { get; } + public WriteDelegate WriteActionDirect { get; } public ReadWriteBehavior(ReadDelegate readAction, WriteDelegate writeAction) { - ReadAction = readAction; - WriteAction = writeAction; + ReadAction = (inc, attribute, bitField) => readAction(inc, attribute, bitField); + WriteAction = (o, attribute, msg, bitField) => writeAction((T)o!, attribute, msg, bitField); + ReadActionDirect = readAction; + WriteActionDirect = writeAction; } } @@ -80,17 +96,18 @@ namespace Barotrauma public delegate void SetValueDelegate(object? obj, object? value); + public readonly string Name; public readonly Type Type; - public readonly ReadWriteBehavior Behavior; + public readonly IReadWriteBehavior Behavior; public readonly NetworkSerialize Attribute; public readonly SetValueDelegate SetValue; public readonly GetValueDelegate GetValue; public readonly bool HasOwnAttribute; - public CachedReflectedVariable(MemberInfo info, ReadWriteBehavior behavior, Type baseClassType) + public CachedReflectedVariable(MemberInfo info, IReadWriteBehavior behavior, Type baseClassType) { Behavior = behavior; - + Name = info.Name; switch (info) { case PropertyInfo pi: @@ -126,343 +143,373 @@ namespace Barotrauma private static readonly Dictionary> CachedVariables = new Dictionary>(); - private static readonly ImmutableDictionary TypeBehaviors = new Dictionary - { - { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) }, - { typeof(Byte), new ReadWriteBehavior(ReadByte, WriteDynamic) }, - { typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteDynamic) }, - { typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteDynamic) }, - { typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteDynamic) }, - { typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) }, - { typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteDynamic) }, - { typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteDynamic) }, - { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, - { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) }, - { typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) }, - { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteDynamic) }, - { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, - { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } - }.ToImmutableDictionary(); + private static readonly Dictionary TypeBehaviors + = new Dictionary + { + { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteBoolean) }, + { typeof(Byte), new ReadWriteBehavior(ReadByte, WriteByte) }, + { typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteUInt16) }, + { typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteInt16) }, + { typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteUInt32) }, + { typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) }, + { typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteUInt64) }, + { typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteInt64) }, + { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, + { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDouble) }, + { typeof(String), new ReadWriteBehavior(ReadString, WriteString) }, + { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteIdentifier) }, + { typeof(AccountId), new ReadWriteBehavior(ReadAccountId, WriteAccountId) }, + { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, + { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } + }; - private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> + private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> { // Arrays - { type => typeof(Array).IsAssignableFrom(type.BaseType), new ReadWriteBehavior(ReadArray, WriteArray) }, + { type => type.IsArray, CreateArrayBehavior }, // Nested INetSerializableStructs - { type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) }, + { type => typeof(INetSerializableStruct).IsAssignableFrom(type), CreateINetSerializableStructBehavior }, // Enums - { type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) }, + { type => type.IsEnum, CreateEnumBehavior }, // Nullable - { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }, + { type => Nullable.GetUnderlyingType(type) != null, CreateNullableStructBehavior }, + + // ImmutableArray + { type => IsOfGenericType(type, typeof(ImmutableArray<>)), CreateImmutableArrayBehavior }, // Option - { type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } + { type => IsOfGenericType(type, typeof(Option<>)), CreateOptionBehavior } }.ToImmutableDictionary(); - private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); - - private static readonly Dictionary cachedSomeCreateMethods = new Dictionary(); - private static readonly Dictionary cachedNoneCreateMethod = new Dictionary(); - - private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => - throw new SerializationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); - - private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new SerializationException($"Type {type} cannot be deserialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); - - private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + /// The type that the behavior handles + /// The type that will be used as the generic parameter for the read/write methods + /// The read method. + /// It must have a generic parameter. + /// The return type must be such that if the generic parameter is replaced with funcGenericParam, you get behaviorGenericParam. + /// The write method. The first parameter's type must be the same as readFunc's return type. + /// Ideally the least specific type possible, because it's replaced by behaviorGenericParam + /// A ReadWriteBehavior<behaviorGenericParam> + private static IReadWriteBehavior CreateBehavior(Type behaviorGenericParam, + Type funcGenericParam, + ReadWriteBehavior.ReadDelegate readFunc, + ReadWriteBehavior.WriteDelegate writeFunc) { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + var behaviorType = typeof(ReadWriteBehavior<>).MakeGenericType(behaviorGenericParam); - Type type = obj.GetType(); - Type optionType = type.GetGenericTypeDefinition(); - Type underlyingType = type.GetGenericArguments()[0]; + var readDelegateType = typeof(ReadWriteBehavior<>.ReadDelegate).MakeGenericType(behaviorGenericParam); + var writeDelegateType = typeof(ReadWriteBehavior<>.WriteDelegate).MakeGenericType(behaviorGenericParam); - if (optionType == typeof(None<>)) + var constructor = behaviorType.GetConstructor(new[] { - msg.Write(false); + readDelegateType, writeDelegateType + }); + + return (constructor!.Invoke(new object[] + { + readFunc.Method.GetGenericMethodDefinition().MakeGenericMethod(funcGenericParam).CreateDelegate(readDelegateType), + writeFunc.Method.GetGenericMethodDefinition().MakeGenericMethod(funcGenericParam).CreateDelegate(writeDelegateType) + }) as IReadWriteBehavior)!; + } + + private static IReadWriteBehavior CreateArrayBehavior(Type arrayType) => + CreateBehavior( + arrayType, + arrayType.GetElementType()!, + ReadArray, + WriteArray); + + private static IReadWriteBehavior CreateINetSerializableStructBehavior(Type structType) => + CreateBehavior( + structType, + structType, + ReadINetSerializableStruct, + WriteINetSerializableStruct); + + private static IReadWriteBehavior CreateEnumBehavior(Type enumType) => + CreateBehavior( + enumType, + enumType, + ReadEnum, + WriteEnum); + + private static IReadWriteBehavior CreateNullableStructBehavior(Type nullableType) => + CreateBehavior( + nullableType, + Nullable.GetUnderlyingType(nullableType)!, + ReadNullable, + WriteNullable); + + private static IReadWriteBehavior CreateOptionBehavior(Type optionType) => + CreateBehavior( + optionType, + optionType.GetGenericArguments()[0], + ReadOption, + WriteOption); + + private static IReadWriteBehavior CreateImmutableArrayBehavior(Type arrayType) => + CreateBehavior( + arrayType, + arrayType.GetGenericArguments()[0], + ReadImmutableArray, + WriteImmutableArray); + + private static ImmutableArray ReadImmutableArray(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + { + return ReadArray(inc, attribute, bitField).ToImmutableArray(); + } + + private static void WriteImmutableArray(ImmutableArray array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + { + ToolBox.ThrowIfNull(array); + WriteIReadOnlyCollection(array, attribute, msg, bitField); + } + + private static T[] ReadArray(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + { + int length = bitField.ReadInteger(0, attribute.ArrayMaxSize); + + T[] array = new T[length]; + + if (!TryFindBehavior(out ReadWriteBehavior behavior)) + { + throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(ReadArray)}"); } - else if (optionType == typeof(Some<>)) + + for (int i = 0; i < length; i++) { - msg.Write(true); - if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) - { - behavior.WriteAction(obj.Value, attribute, msg); - } + array[i] = behavior.ReadActionDirect(inc, attribute, bitField); } - else + + return array; + } + + private static void WriteArray(T[] array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + { + ToolBox.ThrowIfNull(array); + WriteIReadOnlyCollection(array, attribute, msg, bitField); + } + + private static void WriteIReadOnlyCollection(IReadOnlyCollection array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + { + bitField.WriteInteger(array.Count, 0, attribute.ArrayMaxSize); + + if (!TryFindBehavior(out ReadWriteBehavior behavior)) { - throw new ArgumentOutOfRangeException(nameof(obj), "Option type was neither None or Some"); + throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(WriteArray)}"); + } + + foreach (T o in array) + { + behavior.WriteActionDirect(o, attribute, msg, bitField); } } - private static dynamic? ReadOption(IReadMessage inc, Type type, NetworkSerialize attribute) + private static T ReadINetSerializableStruct(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : INetSerializableStruct { - Type underlyingType = type.GetGenericArguments()[0]; - bool hasValue = inc.ReadBoolean(); - if (!hasValue) - { - return GetCreateMethod(typeof(None<>), underlyingType, cachedNoneCreateMethod).Invoke(null, null); - } - - if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) - { - dynamic? value = behavior.ReadAction(inc, underlyingType, attribute); - return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new[] { value }); - } - - throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}"); - - static MethodInfo GetCreateMethod(Type optionType, Type type, Dictionary cache) - { - if (cache.TryGetValue(type, out MethodInfo? foundInfo)) - { - return foundInfo; - } - - Type genericType = optionType.MakeGenericType(type); - MethodInfo info = genericType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public)!; - cache.Add(type, info); - return info; - } + return INetSerializableStruct.ReadInternal(inc, bitField); } - private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteINetSerializableStruct(T serializableStruct, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : INetSerializableStruct { - if (obj is { } notNull) - { - msg.Write(true); - - if (TryFindBehavior(notNull.GetType(), out ReadWriteBehavior behavior)) - { - // uh oh, something terrible has happened! - if (behavior.WriteAction == WriteNullable) { behavior = InvalidReadWriteBehavior; } - - behavior.WriteAction(notNull, attribute, msg); - return; - } - } - - msg.Write(false); + ToolBox.ThrowIfNull(serializableStruct); + serializableStruct.WriteInternal(msg, bitField); } - private static dynamic? ReadNullable(IReadMessage inc, Type type, NetworkSerialize attribute) + private static T ReadEnum(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : Enum { - if (!inc.ReadBoolean()) { return null; } + var type = typeof(T); - Type? underlyingType = Nullable.GetUnderlyingType(type); - if (underlyingType is null) { throw new InvalidOperationException($"Could not get the underlying type of {type} in {nameof(ReadNullable)}"); } - - if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) - { - // uh oh, something terrible has happened! - if (behavior.ReadAction == ReadNullable) { behavior = InvalidReadWriteBehavior; } - - return behavior.ReadAction(inc, underlyingType, attribute); - } - - throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadNullable)}"); - } - - private static void WriteEnum(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) - { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } - - Range range = GetEnumRange(obj.GetType()); - msg.WriteRangedInteger(Convert.ChangeType(obj, obj.GetTypeCode()), range.Start, range.End); - } - - private static dynamic ReadEnum(IReadMessage inc, Type type, NetworkSerialize attribute) - { Range range = GetEnumRange(type); - int enumIndex = inc.ReadRangedInteger(range.Start, range.End); + int enumIndex = bitField.ReadInteger(range.Start, range.End); - foreach (dynamic? e in Enum.GetValues(type)) + if (typeof(T).GetCustomAttribute() != null) { - if (Convert.ChangeType(e, e!.GetTypeCode()) == enumIndex) { return e; } + return (T)(object)enumIndex; + } + + foreach (T e in (T[])Enum.GetValues(type)) + { + if (((int)(object)e) == enumIndex) { return e; } } throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); } - private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteEnum(T value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : Enum { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + ToolBox.ThrowIfNull(value); - if (!(obj is INetSerializableStruct serializableStruct)) { throw new InvalidOperationException($"Object in {nameof(WriteINetSerializableStruct)} was {obj.GetType()} but expected {nameof(INetSerializableStruct)}"); } - - serializableStruct.Write(msg); + Range range = GetEnumRange(typeof(T)); + bitField.WriteInteger((int)Convert.ChangeType(value, value.GetTypeCode()), range.Start, range.End); } - private static dynamic ReadINetSerializableStruct(IReadMessage inc, Type type, NetworkSerialize attribute) - { - return INetSerializableStruct.ReadDynamic(type, inc); - } - - private static void WriteDynamic(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) - { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } - - msg.Write(obj); - } - - private static dynamic ReadArray(IReadMessage inc, Type type, NetworkSerialize attribute) - { - Type? elementType = type.GetElementType(); - if (elementType is null) { throw new InvalidOperationException($"Could not get the element type of {type} in {nameof(ReadArray)}"); } - - int length = inc.ReadRangedInteger(0, attribute.ArrayMaxSize); - - Array list = Array.CreateInstance(elementType, length); - - for (int i = 0; i < length; i++) + private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : struct => + ReadOption(inc, attribute, bitField) switch { - if (TryFindBehavior(elementType, out ReadWriteBehavior behavior)) - { - list.SetValue(behavior.ReadAction(inc, elementType, attribute), i); - } - else - { - throw new InvalidOperationException($"Could not find suitable behavior for type {elementType} in {nameof(ReadArray)}"); - } + Some { Value: var value } => value, + None _ => null, + _ => throw new ArgumentOutOfRangeException() + }; + + private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : struct => + WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); + + private static Option ReadOption(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + { + bool hasValue = bitField.ReadBoolean(); + if (!hasValue) + { + return Option.None(); } - return list; + if (TryFindBehavior(out ReadWriteBehavior behavior)) + { + return Option.Some(behavior.ReadActionDirect(inc, attribute, bitField)); + } + + throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(ReadOption)}"); } - private static void WriteArray(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteOption(Option option, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + ToolBox.ThrowIfNull(option); - if (!(obj is Array array)) { throw new InvalidOperationException($"Object in {nameof(WriteArray)} was {obj.GetType()} but expected {nameof(Array)}"); } - - msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize); - - foreach (dynamic? o in array) + if (option.TryUnwrap(out T value)) { - if (TryFindBehavior(o!.GetType(), out ReadWriteBehavior behavior)) + bitField.WriteBoolean(true); + if (TryFindBehavior(out ReadWriteBehavior behavior)) { - behavior.WriteAction(o, attribute, msg); + behavior.WriteActionDirect(value, attribute, msg, bitField); } } + else + { + bitField.WriteBoolean(false); + } } - private static dynamic ReadBoolean(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadBoolean(); + private static bool ReadBoolean(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => bitField.ReadBoolean(); + private static void WriteBoolean(bool b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { bitField.WriteBoolean(b); } + + private static byte ReadByte(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadByte(); + private static void WriteByte(byte b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteByte(b); } - private static dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte(); + private static ushort ReadUInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt16(); + private static void WriteUInt16(ushort b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt16(b); } - private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16(); + private static short ReadInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt16(); + private static void WriteInt16(short b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteInt16(b); } - private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16(); + private static uint ReadUInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt32(); + private static void WriteUInt32(uint b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt32(b); } - private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32(); - - private static dynamic ReadInt32(IReadMessage inc, Type type, NetworkSerialize attribute) + private static int ReadInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) { if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) { - return inc.ReadRangedInteger(attribute.MinValueInt, attribute.MaxValueInt); + return bitField.ReadInteger(attribute.MinValueInt, attribute.MaxValueInt); } return inc.ReadInt32(); } - private static void WriteInt32(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteInt32(int i, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + ToolBox.ThrowIfNull(i); if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) { - msg.WriteRangedInteger(obj, attribute.MinValueInt, attribute.MaxValueInt); + bitField.WriteInteger(i, attribute.MinValueInt, attribute.MaxValueInt); return; } - msg.Write(obj); + msg.WriteInt32(i); } - private static dynamic ReadUInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt64(); + private static ulong ReadUInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt64(); + private static void WriteUInt64(ulong b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt64(b); } - private static dynamic ReadInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt64(); + private static long ReadInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt64(); + private static void WriteInt64(long b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteInt64(b); } - private static dynamic ReadSingle(IReadMessage inc, Type type, NetworkSerialize attribute) + private static float ReadSingle(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) { if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) { - return inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + return bitField.ReadFloat(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); } return inc.ReadSingle(); } - private static void WriteSingle(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteSingle(float f, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + ToolBox.ThrowIfNull(f); if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) { - msg.WriteRangedSingle(obj, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + bitField.WriteFloat(f, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); return; } - msg.Write(obj); + msg.WriteSingle(f); } - private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); + private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadDouble(); + private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteDouble(b); } - private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); + private static string ReadString(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadString(); + private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteString(b); } - private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier(); + private static Identifier ReadIdentifier(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadIdentifier(); + private static void WriteIdentifier(Identifier b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteIdentifier(b); } - private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); - - private static void WriteColor(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static AccountId ReadAccountId(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + string str = inc.ReadString(); + return AccountId.Parse(str).TryUnwrap(out var accountId) + ? accountId + : throw new InvalidCastException($"Could not parse \"{str}\" as an {nameof(AccountId)}"); + } + + private static void WriteAccountId(AccountId accountId, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + { + msg.WriteString(accountId.StringRepresentation); + } + + private static Color ReadColor(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); + + private static void WriteColor(Color color, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + { + ToolBox.ThrowIfNull(color); if (attribute.IncludeColorAlpha) { - msg.WriteColorR8G8B8A8(obj); + msg.WriteColorR8G8B8A8(color); return; } - msg.WriteColorR8G8B8(obj); + msg.WriteColorR8G8B8(color); } - private static dynamic ReadVector2(IReadMessage inc, Type type, NetworkSerialize attribute) + private static Vector2 ReadVector2(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) { - float x; - float y; - - if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) - { - x = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); - y = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); - } - else - { - x = inc.ReadSingle(); - y = inc.ReadSingle(); - } + float x = ReadSingle(inc, attribute, bitField); + float y = ReadSingle(inc, attribute, bitField); return new Vector2(x, y); } - private static void WriteVector2(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + private static void WriteVector2(Vector2 vector2, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { - if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + ToolBox.ThrowIfNull(vector2); - var (x, y) = (Vector2)obj; - if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) - { - msg.WriteRangedSingle(x, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); - msg.WriteRangedSingle(y, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); - return; - } - - msg.Write(x); - msg.Write(y); + var (x, y) = vector2; + WriteSingle(x, attribute, msg, bitField); + WriteSingle(y, attribute, msg, bitField); } private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue; @@ -474,53 +521,71 @@ namespace Barotrauma return new Range(values.Min(), values.Max()); } - private static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) + private static bool TryFindBehavior(out ReadWriteBehavior behavior) where T : notnull { - if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; } + bool found = TryFindBehavior(typeof(T), out var bhvr); + behavior = found ? (ReadWriteBehavior)bhvr : default; + return found; + } - foreach (var (predicate, behavior2) in TypePredicates) + private static bool TryFindBehavior(Type type, out IReadWriteBehavior behavior) + { + if (TypeBehaviors.TryGetValue(type, out var outBehavior)) { - if (predicate(type)) - { - behavior = behavior2; - return true; - } + behavior = outBehavior; + return true; } - behavior = InvalidReadWriteBehavior; + foreach (var (predicate, factory) in BehaviorFactories) + { + if (!predicate(type)) { continue; } + + behavior = factory(type); + TypeBehaviors.Add(type, behavior); + return true; + } + + behavior = default!; return false; } - public static ImmutableArray GetPropertiesAndFields(Type type, Type baseClassType) + public static ImmutableArray GetPropertiesAndFields(Type type) { if (CachedVariables.TryGetValue(type, out var cached)) { return cached; } List variables = new List(); - IEnumerable propertyInfos = type.GetProperties().Where(HasAttribute); - IEnumerable fieldInfos = type.GetFields().Where(HasAttribute); + IEnumerable propertyInfos = type.GetProperties().Where(HasAttribute).Where(NotStatic); + IEnumerable fieldInfos = type.GetFields().Where(HasAttribute).Where(NotStatic); foreach (PropertyInfo info in propertyInfos) { - if (TryFindBehavior(info.PropertyType, out ReadWriteBehavior behavior)) + if (info.SetMethod is null) { - variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + //skip get-only properties, because it's + //useful to have them but their value + //cannot be set when reading a struct + continue; + } + if (TryFindBehavior(info.PropertyType, out IReadWriteBehavior behavior)) + { + variables.Add(new CachedReflectedVariable(info, behavior, type)); } else { - throw new SerializationException($"Unable to serialize type \"{type}\"."); + throw new Exception($"Unable to serialize type \"{type}\"."); } } foreach (FieldInfo info in fieldInfos) { - if (TryFindBehavior(info.FieldType, out ReadWriteBehavior behavior)) + if (TryFindBehavior(info.FieldType, out IReadWriteBehavior behavior)) { - variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + variables.Add(new CachedReflectedVariable(info, behavior, type)); } else { - throw new SerializationException($"Unable to serialize type \"{type}\"."); + throw new Exception($"Unable to serialize type \"{type}\"."); } } @@ -528,7 +593,20 @@ namespace Barotrauma CachedVariables.Add(type, array); return array; - bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute() ?? baseClassType.GetCustomAttribute()) != null; + bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute() ?? type.GetCustomAttribute()) != null; + + static bool NotStatic(MemberInfo info) + => info switch + { + PropertyInfo property => property.GetGetMethod() is { IsStatic: false }, + FieldInfo field => !field.IsStatic, + _ => false + }; + } + + private static bool IsOfGenericType(Type type, Type comparedTo) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == comparedTo; } } @@ -575,13 +653,15 @@ namespace Barotrauma /// float
/// double
/// string
+ ///
+ ///
///
///
/// In addition arrays, enums, and are supported.
/// Using or will make the field or property optional. /// /// - public interface INetSerializableStruct + internal interface INetSerializableStruct { /// /// Deserializes a network message into a struct. @@ -608,21 +688,34 @@ namespace Barotrauma /// Incoming network message /// Type of the struct that implements /// A new struct of type T with fields and properties deserialized - public static T Read(IReadMessage inc) where T : INetSerializableStruct => (T)ReadDynamic(typeof(T), inc); - - public static dynamic ReadDynamic(Type type, IReadMessage inc) + public static T Read(IReadMessage inc) where T : INetSerializableStruct { - object? newObject = Activator.CreateInstance(type); + IReadableBitField bitField = new ReadOnlyBitField(inc); + return ReadInternal(inc, bitField); + } + + public static T ReadInternal(IReadMessage inc, IReadableBitField bitField) where T : INetSerializableStruct + { + object? newObject = Activator.CreateInstance(typeof(T)); if (newObject is null) { return default!; } - var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + var properties = NetSerializableProperties.GetPropertiesAndFields(typeof(T)); foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize attribute = property.Attribute; - property.SetValue(newObject, property.Behavior.ReadAction(inc, property.Type, attribute)); + object? value = property.Behavior.ReadAction(inc, property.Attribute, bitField); + try + { + property.SetValue(newObject, value); + } + catch (Exception exception) + { + throw new Exception($"Failed to assign" + + $" {value ?? "[NULL]"} ({value?.GetType().Name ?? "[NULL]"})" + + $" to {typeof(T).Name}.{property.Name} ({property.Type.Name})", exception); + } } - return newObject; + return (T)newObject; } /// @@ -651,34 +744,22 @@ namespace Barotrauma /// Outgoing network message public void Write(IWriteMessage msg) { - Type type = GetType(); - var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + IWritableBitField bitField = new WriteOnlyBitField(); + IWriteMessage structWriteMsg = new WriteOnlyMessage(); + WriteInternal(structWriteMsg, bitField); + bitField.WriteToMessage(msg); + msg.WriteBytes(structWriteMsg.Buffer, 0, structWriteMsg.LengthBytes); + } + + public void WriteInternal(IWriteMessage msg, IWritableBitField bitField) + { + var properties = NetSerializableProperties.GetPropertiesAndFields(GetType()); + foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize attribute = property.Attribute; - property.Behavior.WriteAction(property.GetValue(this), attribute, msg); + object? value = property.GetValue(this); + property.Behavior.WriteAction(value!, property.Attribute, msg, bitField); } } } - - public static class WriteOnlyMessageExtensions - { -#if CLIENT - public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header) - { - msg.Write((byte)header); - return msg; - } -#elif SERVER - public static IWriteMessage WithHeader(this IWriteMessage msg, ServerPacketHeader header) - { - msg.Write((byte)header); - return msg; - } -#endif - public static void Write(this IWriteMessage msg, INetSerializableStruct serializableStruct) - { - serializableStruct.Write(msg); - } - } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetBufferExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetBufferExtensions.cs deleted file mode 100644 index 0e407cc0c..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetBufferExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Barotrauma.Networking -{ - static class NetBufferExtensions - { - //public static void WriteEnum(this NetBuffer buffer, Enum value) - //{ - // buffer.WriteRangedInteger(0, Enum.GetValues(value.GetType()).Length - 1, Convert.ToInt32(value)); - //} - - //public static TEnum ReadEnum(this NetBuffer buffer) - //{ - // return (TEnum)(object)buffer.ReadRangedInteger(0, Enum.GetValues(typeof(TEnum)).Length - 1); - //} - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index 0b154b322..c88d1c267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Networking //write an empty event to avoid messing up IDs //(otherwise the clients might read the next event in the message and think its ID //is consecutive to the previous one, even though we skipped over this broken event) - tempBuffer.Write(Entity.NullEntityID); + tempBuffer.WriteUInt16(Entity.NullEntityID); eventCount++; continue; } @@ -49,9 +49,9 @@ namespace Barotrauma.Networking break; } - tempBuffer.Write(e.EntityID); + tempBuffer.WriteUInt16(e.EntityID); tempBuffer.WriteVariableUInt32((uint)tempEventBuffer.LengthBytes); - tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); + tempBuffer.WriteBytes(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); sentEvents.Add(e); eventCount++; @@ -60,9 +60,9 @@ namespace Barotrauma.Networking if (eventCount > 0) { msg.WritePadBits(); - msg.Write(eventsToSync[0].ID); - msg.Write((byte)eventCount); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + msg.WriteUInt16(eventsToSync[0].ID); + msg.WriteByte((byte)eventCount); + msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs index be2a6d9df..e75202086 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs @@ -28,6 +28,19 @@ namespace Barotrauma.Networking public static bool IdMoreRecentOrMatches(ushort newId, ushort oldId) => !IdMoreRecent(oldId, newId); + /// + /// Returns some ID that is older than the input ID. There are no guarantees + /// regarding its relation to values other than the input. + /// + public static ushort GetIdOlderThan(ushort id) +#if DEBUG + // Debug implementation has some RNG to discourage bad assumptions about the return value + => unchecked((ushort)(id - 1 - Rand.Int(500, sync: Rand.RandSync.Unsynced))); +#else + // Release implementation favors performance + => unchecked((ushort)(id - 1)); +#endif + public static ushort Difference(ushort id1, ushort id2) { int diff = id2 > id1 ? id2 - id1 : id1 - id2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index e5e7d04b1..79fb09932 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -135,29 +135,29 @@ namespace Barotrauma.Networking enum DisconnectReason { + //do not attempt reconnecting with these reasons Unknown, + Disconnected, Banned, Kicked, ServerShutdown, ServerCrashed, ServerFull, AuthenticationRequired, - SteamAuthenticationRequired, SteamAuthenticationFailed, SessionTaken, TooManyFailedLogins, - NoName, InvalidName, NameTaken, InvalidVersion, - MissingContentPackage, - IncompatibleContentPackage, - NotOnWhitelist, + SteamP2PError, + + //attempt reconnecting with these reasons + Timeout, ExcessiveDesyncOldEvent, ExcessiveDesyncRemovedEvent, SyncTimeout, - SteamP2PError, - SteamP2PTimeOut, + SteamP2PTimeOut } abstract partial class NetworkMember @@ -168,74 +168,38 @@ namespace Barotrauma.Networking set; } - public virtual bool IsServer - { - get { return false; } - } + public abstract bool IsServer { get; } - public virtual bool IsClient - { - get { return false; } - } + public abstract bool IsClient { get; } public abstract void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null); -#if DEBUG - public Dictionary messageCount = new Dictionary(); -#endif - - protected ServerSettings serverSettings; + public abstract Voting Voting { get; } - public Voting Voting { get; protected set; } - - protected TimeSpan updateInterval; protected DateTime updateTimer; - protected bool gameStarted; - - protected RespawnManager respawnManager; - public bool ShowNetStats; public float SimulatedRandomLatency, SimulatedMinimumLatency; public float SimulatedLoss; public float SimulatedDuplicatesChance; - public int TickRate - { - get { return serverSettings.TickRate; } - set - { - serverSettings.TickRate = MathHelper.Clamp(value, 1, 60); - updateInterval = new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / serverSettings.TickRate, 1, 500)); - } - } - public KarmaManager KarmaManager { get; private set; } = new KarmaManager(); - public bool GameStarted - { - get { return gameStarted; } - } + public bool GameStarted { get; protected set; } - public virtual List ConnectedClients - { - get { return null; } - } + public abstract IReadOnlyList ConnectedClients { get; } - public RespawnManager RespawnManager - { - get { return respawnManager; } - } + public RespawnManager RespawnManager { get; protected set; } + + public ServerSettings ServerSettings { get; protected set; } + + public TimeSpan UpdateInterval => new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / ServerSettings.TickRate, 1, 500)); - public ServerSettings ServerSettings - { - get { return serverSettings; } - } public bool CanUseRadio(Character sender) { @@ -272,33 +236,18 @@ namespace Barotrauma.Networking { retVal += "color:#ff9900;"; } - retVal += "metadata:" + (client.SteamID != 0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name).Replace("‖", "") + "‖end‖"; + retVal += "metadata:" + (client.AccountId.TryUnwrap(out var accountId) ? accountId.ToString() : client.SessionId.ToString()) + + "‖" + (name ?? client.Name).Replace("‖", "") + "‖end‖"; return retVal; } - public virtual void KickPlayer(string kickedName, string reason) { } + public abstract void KickPlayer(string kickedName, string reason); - public virtual void BanPlayer(string kickedName, string reason, bool range = false, TimeSpan? duration = null) { } + public abstract void BanPlayer(string kickedName, string reason, TimeSpan? duration = null); - public virtual void UnbanPlayer(string playerName, string playerIP) { } - - public virtual void Update(float deltaTime) { } - - public virtual void Disconnect() { } - - /// - /// Check if the two version are compatible (= if they can play together in multiplayer). - /// Returns null if compatibility could not be determined (invalid/unknown version number). - /// - public static bool? IsCompatible(string myVersion, string remoteVersion) - { - if (string.IsNullOrEmpty(myVersion) || string.IsNullOrEmpty(remoteVersion)) { return null; } - - if (!Version.TryParse(myVersion, out Version myVersionNumber)) { return null; } - if (!Version.TryParse(remoteVersion, out Version remoteVersionNumber)) { return null; } - - return IsCompatible(myVersionNumber, remoteVersionNumber); - } + public abstract void UnbanPlayer(string playerName); + + public abstract void UnbanPlayer(Endpoint endpoint); /// /// Check if the two version are compatible (= if they can play together in multiplayer). diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index af78cac5f..d4b897c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -47,32 +47,32 @@ namespace Barotrauma.Networking public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, bool isNewOrder) { - msg.Write(order.Prefab.Identifier); - msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); - msg.Write(order.TargetSpatialEntity is Entity ? (order.TargetEntity as Entity).ID : (UInt16)0); + msg.WriteIdentifier(order.Prefab.Identifier); + msg.WriteUInt16(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); + msg.WriteUInt16(order.TargetSpatialEntity is Entity ? (order.TargetEntity as Entity).ID : (UInt16)0); // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously if (!order.IsDismissal) { - msg.Write((byte)order.Options.IndexOf(order.Option)); + msg.WriteByte((byte)order.Options.IndexOf(order.Option)); } else { if (order.Option != Identifier.Empty) { - msg.Write(true); + msg.WriteBoolean(true); string[] dismissedOrder = order.Option.Value.Split('.'); - msg.Write((byte)dismissedOrder.Length); + msg.WriteByte((byte)dismissedOrder.Length); if (dismissedOrder.Length > 0) { Identifier dismissedOrderIdentifier = dismissedOrder[0].ToIdentifier(); var orderPrefab = OrderPrefab.Prefabs[dismissedOrderIdentifier]; - msg.Write(dismissedOrderIdentifier); + msg.WriteIdentifier(dismissedOrderIdentifier); if (dismissedOrder.Length > 1) { Identifier dismissedOrderOption = dismissedOrder[1].ToIdentifier(); - msg.Write((byte)orderPrefab.Options.IndexOf(dismissedOrderOption)); + msg.WriteByte((byte)orderPrefab.Options.IndexOf(dismissedOrderOption)); } } } @@ -80,29 +80,29 @@ namespace Barotrauma.Networking { // If the order option is not specified for a Dismiss order, // we dismiss all current orders for the character - msg.Write(false); + msg.WriteBoolean(false); } } - msg.Write((byte)order.ManualPriority); - msg.Write((byte)order.TargetType); + msg.WriteByte((byte)order.ManualPriority); + msg.WriteByte((byte)order.TargetType); if (order.TargetType == Order.OrderTargetType.Position && order.TargetSpatialEntity is OrderTarget orderTarget) { - msg.Write(true); - msg.Write(orderTarget.Position.X); - msg.Write(orderTarget.Position.Y); - msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); + msg.WriteBoolean(true); + msg.WriteSingle(orderTarget.Position.X); + msg.WriteSingle(orderTarget.Position.Y); + msg.WriteUInt16(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); } else { - msg.Write(false); + msg.WriteBoolean(false); if (order.TargetType == Order.OrderTargetType.WallSection) { - msg.Write((byte)(order.WallSectionIndex ?? 0)); + msg.WriteByte((byte)(order.WallSectionIndex ?? 0)); } } - msg.Write(isNewOrder); + msg.WriteBoolean(isNewOrder); } private void WriteOrder(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs new file mode 100644 index 000000000..47fc84fd3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs @@ -0,0 +1,24 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + abstract class AccountId + { + public abstract string StringRepresentation { get; } + + public static Option Parse(string str) + => ReflectionUtils.ParseDerived(str); + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public override string ToString() => StringRepresentation; + + public static bool operator ==(AccountId a, AccountId b) + => a.Equals(b); + + public static bool operator !=(AccountId a, AccountId b) + => !(a == b); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs new file mode 100644 index 000000000..5ae6f4fe5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs @@ -0,0 +1,121 @@ +#nullable enable +using System; + +namespace Barotrauma.Networking +{ + sealed class SteamId : AccountId + { + public readonly UInt64 Value; + + public override string StringRepresentation { get; } + + /// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID + /// ------------------------------------------------------------------------------------ + /// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows: + /// + /// | a | b | c | d | + /// Most significant - | 01 | 1 | 00001 | 0546779D | - Least significant + /// + /// a) 8 bits representing the universe the account belongs to. + /// b) 4 bits representing the type of account. Typically 1. + /// c) 20 bits representing the instance of the account. Typically 1. + /// d) 32 bits representing the account number. + /// + /// The account number is additionally broken up as follows: + /// + /// | e | f | + /// Most significant - | 0000010101000110011101111001110 | 1 | - Least significant + /// + /// e) These are the 31 most significant bits of the account number. + /// f) This is the least significant bit of the account number, discriminated under the name Y for some reason. + /// + /// Barotrauma supports two textual representations of SteamIDs: + /// 1. STEAM40: Given this name as it represents 40 of the 64 bits in the ID. The account type and instance both + /// have an implied value of 1. The format is "STEAM_{universe}:{Y}:{restOfAccountNumber}". + /// 2. STEAM64: If STEAM40 does not suffice to represent an ID (i.e. the account type or instance were different + /// from 1), we use "STEAM64_{fullId}" where fullId is the 64-bit decimal representation of the full + /// ID. + + private const string steam64Prefix = "STEAM64_"; + private const string steam40Prefix = "STEAM_"; + + private const UInt64 usualAccountInstance = 1; + private const UInt64 usualAccountType = 1; + + static UInt64 ExtractBits(UInt64 id, int offset, int numberOfBits) + => (id >> offset) & ((1ul << numberOfBits) - 1ul); + + static UInt64 ExtractY(UInt64 id) + => ExtractBits(id, offset: 0, numberOfBits: 1); + static UInt64 ExtractAccountNumberRemainder(UInt64 id) + => ExtractBits(id, offset: 1, numberOfBits: 31); + static UInt64 ExtractAccountInstance(UInt64 id) + => ExtractBits(id, offset: 32, numberOfBits: 20); + static UInt64 ExtractAccountType(UInt64 id) + => ExtractBits(id, offset: 52, numberOfBits: 4); + static UInt64 ExtractUniverse(UInt64 id) + => ExtractBits(id, offset: 56, numberOfBits: 8); + + public SteamId(UInt64 value) + { + Value = value; + + if (ExtractAccountInstance(Value) == usualAccountInstance + && ExtractAccountType(Value) == usualAccountType) + { + UInt64 y = ExtractY(Value); + UInt64 accountNumberRemainder = ExtractAccountNumberRemainder(Value); + UInt64 universe = ExtractUniverse(Value); + StringRepresentation = $"{steam40Prefix}{universe}:{y}:{accountNumberRemainder}"; + } + else + { + StringRepresentation = $"{steam64Prefix}{Value}"; + } + } + + public override string ToString() => StringRepresentation; + + public new static Option Parse(string str) + { + if (str.IsNullOrWhiteSpace()) { return Option.None(); } + + if (str.StartsWith(steam64Prefix, StringComparison.InvariantCultureIgnoreCase)) { str = str[steam64Prefix.Length..]; } + if (UInt64.TryParse(str, out UInt64 retVal) && ExtractAccountInstance(retVal) > 0) + { + return Option.Some(new SteamId(retVal)); + } + + if (!str.StartsWith(steam40Prefix, StringComparison.InvariantCultureIgnoreCase)) { return Option.None(); } + string[] split = str[steam40Prefix.Length..].Split(':'); + if (split.Length != 3) { return Option.None(); } + + if (!UInt64.TryParse(split[0], out UInt64 universe)) { return Option.None(); } + if (!UInt64.TryParse(split[1], out UInt64 y)) { return Option.None(); } + if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return Option.None(); } + + return Option.Some( + new SteamId((universe << 56) + | usualAccountType << 52 + | usualAccountInstance << 32 + | (accountNumber << 1) + | y)); + } + + public override bool Equals(object? obj) + => obj switch + { + SteamId otherId => this == otherId, + _ => false + }; + + public override int GetHashCode() + => Value.GetHashCode(); + + public static bool operator ==(SteamId a, SteamId b) + => a.Value == b.Value; + + public static bool operator !=(SteamId a, SteamId b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs new file mode 100644 index 000000000..608486f0a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs @@ -0,0 +1,50 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma.Networking +{ + [NetworkSerialize] + readonly struct AccountInfo : INetSerializableStruct + { + public static readonly AccountInfo None = new AccountInfo(Option.None()); + + /// + /// The primary ID for a given user + /// + public readonly Option AccountId; + + /// + /// Other user IDs that this user might be closely tied to, + /// such as the owner of the current copy of Barotrauma + /// + #warning TODO: make ImmutableArray once feature/inetserializablestruct-improvements gets merged to dev + public readonly AccountId[] OtherMatchingIds; + + public AccountInfo(AccountId accountId, params AccountId[] otherIds) : this(Option.Some(accountId), otherIds) { } + + public AccountInfo(Option accountId, params AccountId[] otherIds) + { + AccountId = accountId; + OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToArray(); + } + + public bool Matches(AccountId accountId) + => AccountId.ValueEquals(accountId) || OtherMatchingIds.Contains(accountId); + + public override bool Equals(object? obj) + => obj switch + { + AccountInfo otherInfo => AccountId == otherInfo.AccountId && OtherMatchingIds.All(otherInfo.OtherMatchingIds.Contains), + _ => false + }; + + public override int GetHashCode() + => AccountId.GetHashCode(); + + public static bool operator ==(AccountInfo a, AccountInfo b) + => a.Equals(b); + + public static bool operator !=(AccountInfo a, AccountInfo b) => !(a == b); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs new file mode 100644 index 000000000..f5ca6da14 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + abstract class Address + { + public abstract string StringRepresentation { get; } + + public static Option
Parse(string str) + => ReflectionUtils.ParseDerived(str); + + public abstract bool IsLocalHost { get; } + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public override string ToString() => StringRepresentation; + + public static bool operator ==(Address a, Address b) + => a.Equals(b); + + public static bool operator !=(Address a, Address b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs new file mode 100644 index 000000000..5fadd7644 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs @@ -0,0 +1,65 @@ +#nullable enable +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Barotrauma.Networking +{ + sealed class LidgrenAddress : Address + { + public readonly IPAddress NetAddress; + + public override string StringRepresentation + => NetAddress.ToString(); + + public override bool IsLocalHost => IPAddress.IsLoopback(NetAddress); + + public LidgrenAddress(IPAddress netAddress) + { + if (IPAddress.IsLoopback(netAddress)) + { + NetAddress = IPAddress.Loopback; + } + else + { + NetAddress = netAddress; + } + } + + public new static Option Parse(string endpointStr) + { + if (endpointStr.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return Option.Some(new LidgrenAddress(IPAddress.Loopback)); + } + else if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint)) + { + return Option.Some(new LidgrenAddress(netEndpoint!)); + } + return Option.None(); + } + + public override bool Equals(object? obj) + => obj switch + { + LidgrenAddress otherAddress => this == otherAddress, + _ => false + }; + + public override int GetHashCode() + => NetAddress.GetHashCode(); + + public static bool operator ==(LidgrenAddress a, LidgrenAddress b) + { + var addressA = a.NetAddress.MapToIPv6(); + var addressB = b.NetAddress.MapToIPv6(); + + if (IPAddress.IsLoopback(addressA) && IPAddress.IsLoopback(addressB)) { return true; } + return addressA.Equals(addressB); + } + + public static bool operator !=(LidgrenAddress a, LidgrenAddress b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs new file mode 100644 index 000000000..1508dc239 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs @@ -0,0 +1,22 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + sealed class PipeAddress : Address + { + public override string StringRepresentation => "PIPE"; + + public override bool IsLocalHost => true; + + public override bool Equals(object? obj) + => obj is PipeAddress; + + public override int GetHashCode() => 1; + + public static bool operator ==(PipeAddress a, PipeAddress b) + => true; + + public static bool operator !=(PipeAddress a, PipeAddress b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs new file mode 100644 index 000000000..641815caa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs @@ -0,0 +1,37 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + sealed class SteamP2PAddress : Address + { + public readonly SteamId SteamId; + + public override string StringRepresentation => SteamId.StringRepresentation; + + public override bool IsLocalHost => false; + + public SteamP2PAddress(SteamId steamId) + { + SteamId = steamId; + } + + public new static Option Parse(string endpointStr) + => SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId)); + + public override bool Equals(object? obj) + => obj switch + { + SteamP2PAddress otherAddress => this == otherAddress, + _ => false + }; + + public override int GetHashCode() + => SteamId.GetHashCode(); + + public static bool operator ==(SteamP2PAddress a, SteamP2PAddress b) + => a.SteamId == b.SteamId; + + public static bool operator !=(SteamP2PAddress a, SteamP2PAddress b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs new file mode 100644 index 000000000..394d9c56d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + sealed class UnknownAddress : Address + { + public override string StringRepresentation => "Hidden"; + + public override bool IsLocalHost => false; + + public override bool Equals(object? obj) + => ReferenceEquals(obj, this); + + public override int GetHashCode() => 1; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs new file mode 100644 index 000000000..f1599e654 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs @@ -0,0 +1,42 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + abstract class Endpoint + { + public abstract string StringRepresentation { get; } + + public abstract LocalizedString ServerTypeString { get; } + + public readonly Address Address; + + public Endpoint(Address address) + { + Address = address; + } + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public override string ToString() => StringRepresentation; + + public static Option Parse(string str) + => ReflectionUtils.ParseDerived(str); + + public static bool operator ==(Endpoint? a, Endpoint? b) + { + if (a is null) + { + return b is null; + } + else + { + return a.Equals(b); + } + } + + public static bool operator !=(Endpoint? a, Endpoint? b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs new file mode 100644 index 000000000..44d20264a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -0,0 +1,62 @@ +#nullable enable +using System.Linq; +using System.Net; + +namespace Barotrauma.Networking +{ + sealed class LidgrenEndpoint : Endpoint + { + public readonly IPEndPoint NetEndpoint; + + public int Port => NetEndpoint.Port; + + public override string StringRepresentation + => NetEndpoint.ToString(); + + public override LocalizedString ServerTypeString { get; } = TextManager.Get("DedicatedServer"); + + public LidgrenEndpoint(IPAddress address, int port) : this(new IPEndPoint(address, port)) { } + + public LidgrenEndpoint(IPEndPoint netEndpoint) : base(new LidgrenAddress(netEndpoint.Address)) + { + NetEndpoint = netEndpoint; + } + + public new static Option Parse(string endpointStr) + { + string hostName = endpointStr; + int port = NetConfig.DefaultPort; + if (endpointStr.Count(c => c == ':') == 1) + { + string[] split = endpointStr.Split(':'); + hostName = split[0]; + port = int.TryParse(split[1], out var tmpPort) ? tmpPort : port; + } + + if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr)) + { + return Option.Some(new LidgrenEndpoint(adr.NetAddress, port)); + } + + return IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint) + ? Option.Some(new LidgrenEndpoint(netEndpoint)) + : Option.None(); + } + + public override bool Equals(object? obj) + => obj switch + { + LidgrenEndpoint otherEndpoint => this == otherEndpoint, + _ => false + }; + + public override int GetHashCode() + => NetEndpoint.GetHashCode(); + + public static bool operator ==(LidgrenEndpoint a, LidgrenEndpoint b) + => a.Address.Equals(b.Address) && a.Port == b.Port; + + public static bool operator !=(LidgrenEndpoint a, LidgrenEndpoint b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs new file mode 100644 index 000000000..93c6fd1da --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs @@ -0,0 +1,37 @@ +#nullable enable + +namespace Barotrauma.Networking +{ + sealed class SteamP2PEndpoint : Endpoint + { + public readonly SteamId SteamId; + + public override string StringRepresentation => SteamId.StringRepresentation; + + public override LocalizedString ServerTypeString { get; } = TextManager.Get("SteamP2PServer"); + + public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId)) + { + SteamId = steamId; + } + + public new static Option Parse(string endpointStr) + => SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId)); + + public override bool Equals(object? obj) + => obj switch + { + SteamP2PEndpoint otherEndpoint => this == otherEndpoint, + _ => false + }; + + public override int GetHashCode() + => SteamId.GetHashCode(); + + public static bool operator ==(SteamP2PEndpoint a, SteamP2PEndpoint b) + => a.SteamId == b.SteamId; + + public static bool operator !=(SteamP2PEndpoint a, SteamP2PEndpoint b) + => !(a == b); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index f9e6b81a2..2bfc7eec4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -4,11 +4,12 @@ using System.Text; namespace Barotrauma.Networking { - public interface IReadMessage + interface IReadMessage { bool ReadBoolean(); void ReadPadBits(); byte ReadByte(); + byte PeekByte(); UInt16 ReadUInt16(); Int16 ReadInt16(); UInt32 ReadUInt32(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index ae32f3bbc..b5721153d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -2,29 +2,29 @@ namespace Barotrauma.Networking { - public interface IWriteMessage + interface IWriteMessage { - void Write(bool val); + void WriteBoolean(bool val); void WritePadBits(); - void Write(byte val); - void Write(Int16 val); - void Write(UInt16 val); - void Write(Int32 val); - void Write(UInt32 val); - void Write(Int64 val); - void Write(UInt64 val); - void Write(Single val); - void Write(Double val); + void WriteByte(byte val); + void WriteInt16(Int16 val); + void WriteUInt16(UInt16 val); + void WriteInt32(Int32 val); + void WriteUInt32(UInt32 val); + void WriteInt64(Int64 val); + void WriteUInt64(UInt64 val); + void WriteSingle(Single val); + void WriteDouble(Double val); void WriteColorR8G8B8(Microsoft.Xna.Framework.Color val); void WriteColorR8G8B8A8(Microsoft.Xna.Framework.Color val); void WriteVariableUInt32(UInt32 val); - void Write(string val); - void Write(Identifier val); + void WriteString(string val); + void WriteIdentifier(Identifier val); void WriteRangedInteger(int val, int min, int max); void WriteRangedSingle(Single val, Single min, Single max, int bitCount); - void Write(byte[] val, int startIndex, int length); + void WriteBytes(byte[] val, int startIndex, int length); - void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength); + byte[] PrepareForSending(bool compressPastThreshold, out bool isCompressed, out int outLength); int BitPosition { get; set; } int BytePosition { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 00711ed04..d4330e11e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -1,7 +1,6 @@ using Lidgren.Network; using System; -using System.Collections.Generic; -using Barotrauma.IO; +using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Text; @@ -11,7 +10,7 @@ namespace Barotrauma.Networking { public static class MsgConstants { - public const int MTU = 1200; + public const int MTU = 1200; //TODO: determine dynamically public const int CompressionThreshold = 1000; public const int InitialBufferSize = 256; public const int BufferOverAllocateAmount = 4; @@ -58,7 +57,7 @@ namespace Barotrauma.Networking bool testVal = MsgReader.ReadBoolean(buf, ref resetPos); if (testVal != val || resetPos != bitPos) { - DebugConsole.ThrowError("Boolean written incorrectly! " + testVal + ", " + val + "; " + resetPos + ", " + bitPos); + DebugConsole.ThrowError($"Boolean written incorrectly! {testVal}, {val}; {resetPos}, {bitPos}"); } #endif } @@ -125,7 +124,7 @@ namespace Barotrauma.Networking SingleUIntUnion su; su.UIntValue = 0; // must initialize every member of the union to avoid warning su.SingleValue = val; - + EnsureBufferSize(ref buf, bitPos + 32); NetBitWriter.WriteUInt32(su.UIntValue, 32, buf, bitPos); @@ -140,50 +139,48 @@ namespace Barotrauma.Networking WriteBytes(ref buf, ref bitPos, bytes, 0, 8); } - internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, Color val) { EnsureBufferSize(ref buf, bitPos + 24); - + Write(ref buf, ref bitPos, val.R); Write(ref buf, ref bitPos, val.G); Write(ref buf, ref bitPos, val.B); } - - internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + + internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, Color val) { EnsureBufferSize(ref buf, bitPos + 32); - + Write(ref buf, ref bitPos, val.R); Write(ref buf, ref bitPos, val.G); Write(ref buf, ref bitPos, val.B); Write(ref buf, ref bitPos, val.A); } - + internal static void Write(ref byte[] buf, ref int bitPos, string val) { if (string.IsNullOrEmpty(val)) { - WriteVariableUInt32(ref buf, ref bitPos, (uint)0); + WriteVariableUInt32(ref buf, ref bitPos, 0u); return; } - + byte[] bytes = Encoding.UTF8.GetBytes(val); WriteVariableUInt32(ref buf, ref bitPos, (uint)bytes.Length); WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); } - internal static int WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) + internal static void WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) { - int retval = 1; - uint remainingValue = (uint)value; + uint remainingValue = value; while (remainingValue >= 0x80) { Write(ref buf, ref bitPos, (byte)(remainingValue | 0x80)); - remainingValue = remainingValue >> 7; - retval++; + remainingValue >>= 7; } + Write(ref buf, ref bitPos, (byte)remainingValue); - return retval; } internal static void WriteRangedInteger(ref byte[] buf, ref int bitPos, int val, int min, int max) @@ -206,7 +203,7 @@ namespace Barotrauma.Networking EnsureBufferSize(ref buf, bitPos + numberOfBits); - NetBitWriter.WriteUInt32((UInt32)((float)maxVal * unit), numberOfBits, buf, bitPos); + NetBitWriter.WriteUInt32((UInt32)(maxVal * unit), numberOfBits, buf, bitPos); bitPos += numberOfBits; } @@ -225,9 +222,10 @@ namespace Barotrauma.Networking buf = new byte[byteLen + MsgConstants.BufferOverAllocateAmount]; return; } + if (buf.Length < byteLen) { - Array.Resize(ref buf, byteLen + MsgConstants.BufferOverAllocateAmount); + Array.Resize(ref buf, byteLen + MsgConstants.BufferOverAllocateAmount); } } } @@ -241,7 +239,7 @@ namespace Barotrauma.Networking return retval > 0; } - internal static void ReadPadBits(byte[] buf, ref int bitPos) + internal static void ReadPadBits(ref int bitPos) { int bitOffset = bitPos % 8; bitPos += (8 - bitOffset) % 8; @@ -254,6 +252,12 @@ namespace Barotrauma.Networking return retval; } + internal static byte PeekByte(byte[] buf, ref int bitPos) + { + byte retval = NetBitWriter.ReadByte(buf, 8, bitPos); + return retval; + } + internal static UInt16 ReadUInt16(byte[] buf, ref int bitPos) { uint retval = NetBitWriter.ReadUInt16(buf, 16, bitPos); @@ -320,15 +324,15 @@ namespace Barotrauma.Networking return BitConverter.ToDouble(bytes, 0); } - internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8(byte[] buf, ref int bitPos) + internal static Color ReadColorR8G8B8(byte[] buf, ref int bitPos) { byte r = ReadByte(buf, ref bitPos); byte g = ReadByte(buf, ref bitPos); byte b = ReadByte(buf, ref bitPos); return new Color(r, g, b, (byte)255); } - - internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(byte[] buf, ref int bitPos) + + internal static Color ReadColorR8G8B8A8(byte[] buf, ref int bitPos) { byte r = ReadByte(buf, ref bitPos); byte g = ReadByte(buf, ref bitPos); @@ -348,8 +352,7 @@ namespace Barotrauma.Networking byte chunk = ReadByte(buf, ref bitPos); result |= (chunk & 0x7f) << shift; shift += 7; - if ((chunk & 0x80) == 0) - return (uint)result; + if ((chunk & 0x80) == 0) { return (uint)result; } } // ouch; failed to find enough bytes; malformed variable length number? @@ -372,23 +375,23 @@ namespace Barotrauma.Networking if ((bitPos & 7) == 0) { // read directly - string retval = System.Text.Encoding.UTF8.GetString(buf, bitPos >> 3, byteLen); + string retval = Encoding.UTF8.GetString(buf, bitPos >> 3, byteLen); bitPos += (8 * byteLen); return retval; } byte[] bytes = ReadBytes(buf, ref bitPos, byteLen); - return System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); } internal static int ReadRangedInteger(byte[] buf, ref int bitPos, int min, int max) { - uint range = (uint)(max - min); - int numBits = NetUtility.BitsToHoldUInt(range); + uint range = (uint)(max - min); + int numBits = NetUtility.BitsToHoldUInt(range); - uint rvalue = NetBitWriter.ReadUInt32(buf, numBits, bitPos); + uint rvalue = NetBitWriter.ReadUInt32(buf, numBits, bitPos); bitPos += numBits; - + return (int)(min + rvalue); } @@ -397,51 +400,33 @@ namespace Barotrauma.Networking int maxInt = (1 << bitCount) - 1; int intVal = ReadRangedInteger(buf, ref bitPos, 0, maxInt); Single range = max - min; - return min + (range * ((Single)intVal) / ((Single)maxInt)); + return min + range * intVal / maxInt; } internal static byte[] ReadBytes(byte[] buf, ref int bitPos, int numberOfBytes) { byte[] retval = new byte[numberOfBytes]; NetBitWriter.ReadBytes(buf, numberOfBytes, bitPos, retval, 0); - bitPos += (8 * numberOfBytes); + bitPos += 8 * numberOfBytes; return retval; } } - public class WriteOnlyMessage : IWriteMessage + internal sealed class WriteOnlyMessage : IWriteMessage { private byte[] buf = new byte[MsgConstants.InitialBufferSize]; - private int seekPos = 0; - private int lengthBits = 0; + private int seekPos; + private int lengthBits; public int BitPosition { - get - { - return seekPos; - } - set - { - seekPos = value; - } + get => seekPos; + set => seekPos = value; } - public int BytePosition - { - get - { - return seekPos / 8; - } - } + public int BytePosition => seekPos / 8; - public byte[] Buffer - { - get - { - return buf; - } - } + public byte[] Buffer => buf; public int LengthBits { @@ -458,15 +443,9 @@ namespace Barotrauma.Networking } } - public int LengthBytes - { - get - { - return (LengthBits + 7) / 8; - } - } + public int LengthBytes => (LengthBits + 7) / 8; - public void Write(bool val) + public void WriteBoolean(bool val) { MsgWriter.Write(ref buf, ref seekPos, val); } @@ -476,47 +455,47 @@ namespace Barotrauma.Networking MsgWriter.WritePadBits(ref buf, ref seekPos); } - public void Write(byte val) + public void WriteByte(byte val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt16 val) + public void WriteUInt16(UInt16 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int16 val) + public void WriteInt16(Int16 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt32 val) + public void WriteUInt32(UInt32 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int32 val) + public void WriteInt32(Int32 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt64 val) + public void WriteUInt64(UInt64 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int64 val) + public void WriteInt64(Int64 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Single val) + public void WriteSingle(Single val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Double val) + public void WriteDouble(Double val) { MsgWriter.Write(ref buf, ref seekPos, val); } @@ -525,7 +504,7 @@ namespace Barotrauma.Networking { MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); } - + public void WriteColorR8G8B8A8(Color val) { MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); @@ -536,14 +515,14 @@ namespace Barotrauma.Networking MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); } - public void Write(String val) + public void WriteString(String val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Identifier val) + public void WriteIdentifier(Identifier val) { - Write(val.Value); + WriteString(val.Value); } public void WriteRangedInteger(int val, int min, int max) @@ -556,85 +535,67 @@ namespace Barotrauma.Networking MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); } - public void Write(byte[] val, int startPos, int length) + public void WriteBytes(byte[] val, int startPos, int length) { MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); } - - public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int length) + + public byte[] PrepareForSending(bool compressPastThreshold, out bool isCompressed, out int length) { + byte[] outBuf; if (LengthBytes <= MsgConstants.CompressionThreshold || !compressPastThreshold) { isCompressed = false; - if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } + outBuf = new byte[LengthBytes]; Array.Copy(buf, outBuf, LengthBytes); length = LengthBytes; } else { - using (System.IO.MemoryStream output = new System.IO.MemoryStream()) + using MemoryStream output = new MemoryStream(); + + using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Fastest)) { - using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Fastest)) - { - dstream.Write(buf, 0, LengthBytes); - } - - byte[] compressedBuf = output.ToArray(); - //don't send the data as compressed if the data takes up more space after compression - //(which may happen when sending a sub/save file that's already been compressed with a better compression ratio) - if (compressedBuf.Length >= outBuf.Length) - { - isCompressed = false; - if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } - Array.Copy(buf, outBuf, LengthBytes); - length = LengthBytes; - } - else - { - isCompressed = true; - if (compressedBuf.Length > outBuf.Length) { Array.Resize(ref outBuf, compressedBuf.Length); } - Array.Copy(compressedBuf, outBuf, compressedBuf.Length); - length = compressedBuf.Length; - DebugConsole.Log("Compressed message: " + LengthBytes + " to " + length); - } + dstream.Write(buf, 0, LengthBytes); + } + + byte[] compressedBuf = output.ToArray(); + //don't send the data as compressed if the data takes up more space after compression + //(which may happen when sending a sub/save file that's already been compressed with a better compression ratio) + if (compressedBuf.Length >= LengthBytes) + { + isCompressed = false; + outBuf = new byte[LengthBytes]; + Array.Copy(buf, outBuf, LengthBytes); + length = LengthBytes; + } + else + { + isCompressed = true; + outBuf = compressedBuf; + length = outBuf.Length; + DebugConsole.Log($"Compressed message: {LengthBytes} to {length}"); } } + + return outBuf; } } - public class ReadOnlyMessage : IReadMessage + internal sealed class ReadOnlyMessage : IReadMessage { - private byte[] buf; - private int seekPos = 0; - private int lengthBits = 0; + private int seekPos; + private int lengthBits; public int BitPosition { - get - { - return seekPos; - } - set - { - seekPos = value; - } + get => seekPos; + set => seekPos = value; } - public int BytePosition - { - get - { - return seekPos / 8; - } - } + public int BytePosition => seekPos / 8; - public byte[] Buffer - { - get - { - return buf; - } - } + public byte[] Buffer { get; } public int LengthBits { @@ -650,124 +611,126 @@ namespace Barotrauma.Networking } } - public int LengthBytes - { - get - { - return (LengthBits + 7) / 8; - } - } + public int LengthBytes => (LengthBits + 7) / 8; - public NetworkConnection Sender { get; private set; } - - public ReadOnlyMessage(byte[] inBuf, bool isCompressed, int startPos, int inLength, NetworkConnection sender) + public NetworkConnection Sender { get; } + + public ReadOnlyMessage(byte[] inBuf, bool isCompressed, int startPos, int byteLength, NetworkConnection sender) { Sender = sender; if (isCompressed) { byte[] decompressedData; - using (System.IO.MemoryStream input = new System.IO.MemoryStream(inBuf, startPos, inLength)) + using (MemoryStream input = new MemoryStream(inBuf, startPos, byteLength)) { - using (System.IO.MemoryStream output = new System.IO.MemoryStream()) + using (MemoryStream output = new MemoryStream()) { using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress)) { dstream.CopyTo(output); } + decompressedData = output.ToArray(); } } - buf = new byte[decompressedData.Length]; + + Buffer = new byte[decompressedData.Length]; try { - Array.Copy(decompressedData, 0, buf, 0, decompressedData.Length); + Array.Copy(decompressedData, 0, Buffer, 0, decompressedData.Length); } catch (ArgumentException e) { - throw new ArgumentException($"Failed to copy the incoming compressed buffer. Source buffer length: {decompressedData.Length}, start position: {0}, length: {decompressedData.Length}, destination buffer length: {buf.Length}.", e); + throw new ArgumentException( + $"Failed to copy the incoming compressed buffer. Source buffer length: {decompressedData.Length}, start position: {0}, length: {decompressedData.Length}, destination buffer length: {Buffer.Length}.", e); } + lengthBits = decompressedData.Length * 8; - DebugConsole.Log("Decompressing message: " + inLength + " to " + LengthBytes); + DebugConsole.Log("Decompressing message: " + byteLength + " to " + LengthBytes); } else { - buf = new byte[inBuf.Length]; + Buffer = new byte[inBuf.Length]; try { - Array.Copy(inBuf, startPos, buf, 0, inLength); + Array.Copy(inBuf, startPos, Buffer, 0, byteLength); } catch (ArgumentException e) { - throw new ArgumentException($"Failed to copy the incoming uncompressed buffer. Source buffer length: {inBuf.Length}, start position: {startPos}, length: {inLength}, destination buffer length: {buf.Length}.", e); + throw new ArgumentException($"Failed to copy the incoming uncompressed buffer. Source buffer length: {inBuf.Length}, start position: {startPos}, length: {byteLength}, destination buffer length: {Buffer.Length}.", e); } - lengthBits = inLength * 8; + + lengthBits = byteLength * 8; } + seekPos = 0; } public bool ReadBoolean() { - return MsgReader.ReadBoolean(buf, ref seekPos); + return MsgReader.ReadBoolean(Buffer, ref seekPos); } - public void ReadPadBits() - { - MsgReader.ReadPadBits(buf, ref seekPos); - } + public void ReadPadBits() { MsgReader.ReadPadBits(ref seekPos); } public byte ReadByte() { - return MsgReader.ReadByte(buf, ref seekPos); + return MsgReader.ReadByte(Buffer, ref seekPos); + } + + public byte PeekByte() + { + return MsgReader.PeekByte(Buffer, ref seekPos); } public UInt16 ReadUInt16() { - return MsgReader.ReadUInt16(buf, ref seekPos); + return MsgReader.ReadUInt16(Buffer, ref seekPos); } public Int16 ReadInt16() { - return MsgReader.ReadInt16(buf, ref seekPos); + return MsgReader.ReadInt16(Buffer, ref seekPos); } public UInt32 ReadUInt32() { - return MsgReader.ReadUInt32(buf, ref seekPos); + return MsgReader.ReadUInt32(Buffer, ref seekPos); } public Int32 ReadInt32() { - return MsgReader.ReadInt32(buf, ref seekPos); + return MsgReader.ReadInt32(Buffer, ref seekPos); } public UInt64 ReadUInt64() { - return MsgReader.ReadUInt64(buf, ref seekPos); + return MsgReader.ReadUInt64(Buffer, ref seekPos); } public Int64 ReadInt64() { - return MsgReader.ReadInt64(buf, ref seekPos); + return MsgReader.ReadInt64(Buffer, ref seekPos); } public Single ReadSingle() { - return MsgReader.ReadSingle(buf, ref seekPos); + return MsgReader.ReadSingle(Buffer, ref seekPos); } public Double ReadDouble() { - return MsgReader.ReadDouble(buf, ref seekPos); + return MsgReader.ReadDouble(Buffer, ref seekPos); } public UInt32 ReadVariableUInt32() { - return MsgReader.ReadVariableUInt32(buf, ref seekPos); + return MsgReader.ReadVariableUInt32(Buffer, ref seekPos); } public String ReadString() { - return MsgReader.ReadString(buf, ref seekPos); + return MsgReader.ReadString(Buffer, ref seekPos); } public Identifier ReadIdentifier() @@ -777,35 +740,35 @@ namespace Barotrauma.Networking public Color ReadColorR8G8B8() { - return MsgReader.ReadColorR8G8B8(buf, ref seekPos); + return MsgReader.ReadColorR8G8B8(Buffer, ref seekPos); } - + public Color ReadColorR8G8B8A8() { - return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); + return MsgReader.ReadColorR8G8B8A8(Buffer, ref seekPos); } public int ReadRangedInteger(int min, int max) { - return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); + return MsgReader.ReadRangedInteger(Buffer, ref seekPos, min, max); } public Single ReadRangedSingle(Single min, Single max, int bitCount) { - return MsgReader.ReadRangedSingle(buf, ref seekPos, min, max, bitCount); + return MsgReader.ReadRangedSingle(Buffer, ref seekPos, min, max, bitCount); } public byte[] ReadBytes(int numberOfBytes) { - return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); + return MsgReader.ReadBytes(Buffer, ref seekPos, numberOfBytes); } } - public class ReadWriteMessage : IWriteMessage, IReadMessage + internal sealed class ReadWriteMessage : IWriteMessage, IReadMessage { private byte[] buf; - private int seekPos = 0; - private int lengthBits = 0; + private int seekPos; + private int lengthBits; public ReadWriteMessage() { @@ -814,40 +777,22 @@ namespace Barotrauma.Networking lengthBits = 0; } - public ReadWriteMessage(byte[] b, int sPos, int lBits, bool copyBuf) + public ReadWriteMessage(byte[] b, int bitPos, int lBits, bool copyBuf) { buf = copyBuf ? (byte[])b.Clone() : b; - seekPos = sPos; + seekPos = bitPos; lengthBits = lBits; } public int BitPosition { - get - { - return seekPos; - } - set - { - seekPos = value; - } + get => seekPos; + set => seekPos = value; } - public int BytePosition - { - get - { - return seekPos / 8; - } - } + public int BytePosition => seekPos / 8; - public byte[] Buffer - { - get - { - return buf; - } - } + public byte[] Buffer => buf; public int LengthBits { @@ -863,17 +808,11 @@ namespace Barotrauma.Networking } } - public int LengthBytes - { - get - { - return (LengthBits + 7) / 8; - } - } + public int LengthBytes => (LengthBits + 7) / 8; - public NetworkConnection Sender { get { return null; } } + public NetworkConnection Sender => null; - public void Write(bool val) + public void WriteBoolean(bool val) { MsgWriter.Write(ref buf, ref seekPos, val); } @@ -883,47 +822,47 @@ namespace Barotrauma.Networking MsgWriter.WritePadBits(ref buf, ref seekPos); } - public void Write(byte val) + public void WriteByte(byte val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt16 val) + public void WriteUInt16(UInt16 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int16 val) + public void WriteInt16(Int16 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt32 val) + public void WriteUInt32(UInt32 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int32 val) + public void WriteInt32(Int32 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(UInt64 val) + public void WriteUInt64(UInt64 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Int64 val) + public void WriteInt64(Int64 val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Single val) + public void WriteSingle(Single val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Double val) + public void WriteDouble(Double val) { MsgWriter.Write(ref buf, ref seekPos, val); } @@ -932,7 +871,7 @@ namespace Barotrauma.Networking { MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); } - + public void WriteColorR8G8B8A8(Color val) { MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); @@ -943,14 +882,14 @@ namespace Barotrauma.Networking MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); } - public void Write(String val) + public void WriteString(String val) { MsgWriter.Write(ref buf, ref seekPos, val); } - public void Write(Identifier val) + public void WriteIdentifier(Identifier val) { - Write(val.Value); + WriteString(val.Value); } public void WriteRangedInteger(int val, int min, int max) @@ -963,7 +902,7 @@ namespace Barotrauma.Networking MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); } - public void Write(byte[] val, int startPos, int length) + public void WriteBytes(byte[] val, int startPos, int length) { MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); } @@ -973,16 +912,18 @@ namespace Barotrauma.Networking return MsgReader.ReadBoolean(buf, ref seekPos); } - public void ReadPadBits() - { - MsgReader.ReadPadBits(buf, ref seekPos); - } + public void ReadPadBits() { MsgReader.ReadPadBits(ref seekPos); } public byte ReadByte() { return MsgReader.ReadByte(buf, ref seekPos); } + public byte PeekByte() + { + return MsgReader.PeekByte(buf, ref seekPos); + } + public UInt16 ReadUInt16() { return MsgReader.ReadUInt16(buf, ref seekPos); @@ -1042,7 +983,7 @@ namespace Barotrauma.Networking { return MsgReader.ReadColorR8G8B8(buf, ref seekPos); } - + public Color ReadColorR8G8B8A8() { return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); @@ -1063,9 +1004,10 @@ namespace Barotrauma.Networking return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); } - public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength) + public byte[] PrepareForSending(bool compressPastThreshold, out bool isCompressed, out int outLength) { throw new InvalidOperationException("ReadWriteMessages are not to be sent"); } + } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs index f255de255..397f4836c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -1,55 +1,14 @@ -using System; -using System.Net; -using Lidgren.Network; +using Lidgren.Network; namespace Barotrauma.Networking { - public class LidgrenConnection : NetworkConnection + sealed class LidgrenConnection : NetworkConnection { - public NetConnection NetConnection { get; private set; } + public readonly NetConnection NetConnection; - public IPEndPoint IPEndPoint => NetConnection.RemoteEndPoint; - - public string IPString + public LidgrenConnection(NetConnection netConnection) : base(new LidgrenEndpoint(netConnection.RemoteEndPoint)) { - get - { - return IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4NoThrow().ToString() : IPEndPoint.Address.ToString(); - } - } - - public UInt16 Port - { - get - { - return (UInt16)IPEndPoint.Port; - } - } - - public LidgrenConnection(string name, NetConnection netConnection, UInt64 steamId) - { - Name = name; NetConnection = netConnection; - SteamID = steamId; - EndPointString = IPString; - } - - public override bool SetSteamIDIfUnknown(UInt64 id) - { - if (SteamID != 0) { return false; } //do not allow the SteamID to be set multiple times - SteamID = id; - return true; - } - - public override bool EndpointMatches(string endPoint) - { - if (IPEndPoint?.Address == null) { return false; } - if (!IPAddress.TryParse(endPoint, out IPAddress addr)) { return false; } - - IPAddress ip1 = IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4() : IPEndPoint.Address; - IPAddress ip2 = addr.IsIPv4MappedToIPv6 ? addr.MapToIPv4() : addr; - - return ip1.ToString() == ip2.ToString(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index f78e60edd..e1a46e55d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -8,57 +8,37 @@ namespace Barotrauma.Networking Disconnected = 0x2 } - public abstract class NetworkConnection + abstract class NetworkConnection { public const double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while public const double TimeoutThresholdInGame = 10.0; - public string Name; + public AccountInfo AccountInfo { get; private set; } = AccountInfo.None; - public UInt64 SteamID - { - get; - protected set; - } - - public UInt64 OwnerSteamID - { - get; - protected set; - } - - public string EndPointString - { - get; - protected set; - } + public readonly Endpoint Endpoint; + [Obsolete("TODO: this doesn't belong in layer 1")] public LanguageIdentifier Language { get; set; } - public abstract bool EndpointMatches(string endPoint); + public NetworkConnection(Endpoint endpoint) + { + Endpoint = endpoint; + } + + public bool EndpointMatches(Endpoint endPoint) + => Endpoint == endPoint; public NetworkConnectionStatus Status = NetworkConnectionStatus.Disconnected; - public virtual bool SetSteamIDIfUnknown(UInt64 id) + public void SetAccountInfo(AccountInfo newInfo) { - //by default, don't allow setting the ID, this is only done - //with Lidgren connections since those are initialized before - //the SteamID can be known; it's set once the Steam auth ticket - //is received by the server. - return false; - } - - public bool SetOwnerSteamIDIfUnknown(UInt64 id) - { - //we know that for both Lidgren and SteamP2P, the - //owner id isn't known until the auth ticket is - //processed, so this method is the same for both - if (OwnerSteamID != 0) { return false; } - OwnerSteamID = id; - return true; + AccountInfo = newInfo; } + + public sealed override string ToString() + => Endpoint.StringRepresentation; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs index ef2fee23c..bae9ac6e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs @@ -1,19 +1,33 @@ -using Barotrauma.Steam; +#nullable enable using System; namespace Barotrauma.Networking { - public class PipeConnection : NetworkConnection + sealed class PipeEndpoint : Endpoint { - public PipeConnection(ulong steamId) - { - EndPointString = "PIPE"; - SteamID = steamId; - } + public override string StringRepresentation => "PIPE"; + + public override LocalizedString ServerTypeString => throw new InvalidOperationException(); - public override bool EndpointMatches(string endPoint) + public PipeEndpoint() : base(new PipeAddress()) { } + + public override bool Equals(object? obj) + => obj is PipeEndpoint; + + public override int GetHashCode() => 1; + + public static bool operator ==(PipeEndpoint a, PipeEndpoint b) + => true; + + public static bool operator !=(PipeEndpoint a, PipeEndpoint b) + => !(a == b); + } + + sealed class PipeConnection : NetworkConnection + { + public PipeConnection(AccountId accountId) : base(new PipeEndpoint()) { - return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID || endPoint == "PIPE"; + SetAccountInfo(new AccountInfo(Option.Some(accountId))); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs index 7e70ad98b..425e1bfd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs @@ -1,18 +1,13 @@ -using Barotrauma.Steam; -using System; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { - public class SteamP2PConnection : NetworkConnection + sealed class SteamP2PConnection : NetworkConnection { public double Timeout = 0.0; - public SteamP2PConnection(string name, UInt64 steamId) + public SteamP2PConnection(SteamId steamId) : this(new SteamP2PEndpoint(steamId)) { } + + public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint) { - SteamID = steamId; - OwnerSteamID = 0; - EndPointString = SteamManager.SteamIDUInt64ToString(SteamID); - Name = name; Heartbeat(); } @@ -25,10 +20,5 @@ namespace Barotrauma.Networking { Timeout = TimeoutThreshold; } - - public override bool EndpointMatches(string endPoint) - { - return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs similarity index 98% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs rename to Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs index 8f8e62d55..03712e66f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs @@ -40,6 +40,7 @@ namespace Barotrauma.Networking public static bool IsCompressed(this PacketHeader h) => h.HasFlag(PacketHeader.IsCompressed); + #warning TODO: remove? public static bool IsConnectionInitializationStep(this PacketHeader h) => h.HasFlag(PacketHeader.IsConnectionInitializationStep); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs new file mode 100644 index 000000000..ee01e2197 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.CompilerServices; +using Lidgren.Network; + +namespace Barotrauma.Networking +{ + internal static class WriteOnlyMessageExtensions + { +#if CLIENT + public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header) + { + msg.WriteByte((byte)header); + return msg; + } +#elif SERVER + public static IWriteMessage WithHeader(this IWriteMessage msg, ServerPacketHeader header) + { + msg.WriteByte((byte)header); + return msg; + } +#endif + public static void WriteNetSerializableStruct(this IWriteMessage msg, INetSerializableStruct serializableStruct) + { + serializableStruct.Write(msg); + } + + public static NetOutgoingMessage ToLidgren(this IWriteMessage msg, NetPeer peer) + { + NetOutgoingMessage outMsg = peer.CreateMessage(); + outMsg.Write(msg.Buffer, 0, msg.LengthBytes); + return outMsg; + } + } + + internal static class NetIncomingMessageExtensions + { + public static T ReadHeader(this NetIncomingMessage msg) where T : Enum + { + byte header = msg.ReadByte(); + return Unsafe.As(ref header); + } + + public static IReadMessage ToReadMessage(this NetIncomingMessage msg) + { + return new ReadWriteMessage(msg.Data, 0, msg.LengthBits, copyBuf: false); + } + } + + internal static class DeliveryMethodExtensions + { + public static NetDeliveryMethod ToLidgren(this DeliveryMethod deliveryMethod) => + deliveryMethod switch + { + DeliveryMethod.Unreliable => NetDeliveryMethod.Unreliable, + DeliveryMethod.Reliable => NetDeliveryMethod.ReliableUnordered, + DeliveryMethod.ReliableOrdered => NetDeliveryMethod.ReliableOrdered, + _ => NetDeliveryMethod.Unreliable + }; + + public static Steamworks.P2PSend ToSteam(this DeliveryMethod deliveryMethod) => + deliveryMethod switch + { + DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable, + DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Unreliable, + _ => Steamworks.P2PSend.Unreliable + }; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs new file mode 100644 index 000000000..0a042c27f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -0,0 +1,311 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +namespace Barotrauma.Networking +{ + [NetworkSerialize] + internal struct PeerPacketHeaders : INetSerializableStruct + { + public DeliveryMethod DeliveryMethod; + public PacketHeader PacketHeader; + public ConnectionInitialization? Initialization; + + public readonly void Deconstruct( + out DeliveryMethod deliveryMethod, + out PacketHeader packetHeader, + out ConnectionInitialization? initialization) + { + deliveryMethod = DeliveryMethod; + packetHeader = PacketHeader; + initialization = Initialization; + } + } + + [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] + internal struct ClientSteamTicketAndVersionPacket : INetSerializableStruct + { + public string Name; + public Option OwnerKey; + + #warning TODO: do something about the type of this + // It probably should be Option but we shouldn't build support for + // writing SteamIDs to INetSerializableStruct; we should consider adding + // attributes to give custom behaviors to specific members of a struct + public Option SteamId; + + public Option SteamAuthTicket; + public string GameVersion; + public Identifier Language; + } + + [NetworkSerialize] + internal struct SteamP2PInitializationRelayPacket : INetSerializableStruct + { + public ulong LobbyID; + public PeerPacketMessage Message; + } + + [NetworkSerialize] + internal struct SteamP2PInitializationOwnerPacket : INetSerializableStruct + { + public string OwnerName; + } + + + [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] + internal struct ServerPeerContentPackageOrderPacket : INetSerializableStruct + { + public string ServerName; + public ImmutableArray ContentPackages; + } + + [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] + internal struct PeerPacketMessage : INetSerializableStruct + { + public byte[] Buffer; + public readonly int Length => Buffer.Length; + + public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false); + public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn); + } + + [NetworkSerialize(ArrayMaxSize = byte.MaxValue)] + internal struct ClientPeerPasswordPacket : INetSerializableStruct + { + public byte[] Password; + } + + [NetworkSerialize] + internal struct ServerPeerPasswordPacket : INetSerializableStruct + { + public Option Salt; + public Option RetriesLeft; + } + + [NetworkSerialize] + internal readonly struct PeerDisconnectPacket : INetSerializableStruct + { + public readonly DisconnectReason DisconnectReason; + + public readonly string AdditionalInformation; + + private PeerDisconnectPacket( + DisconnectReason disconnectReason, + string additionalInformation = "") + { + DisconnectReason = disconnectReason; + AdditionalInformation = additionalInformation; + } + + public LocalizedString ChatMessage(Client c) + { + LocalizedString message = DisconnectReason switch + { + DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer", + "[client]", c.Name), + DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", c.Name), + DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", c.Name), + _ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason", + ("[client]", c.Name), + ("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}"))) + }; + if (!string.IsNullOrEmpty(AdditionalInformation) && + DisconnectReason is DisconnectReason.Banned or DisconnectReason.Kicked) + { + message += " "+ TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation); + } + return message; + } + + + private LocalizedString MsgWithReason + => TextManager.Get($"DisconnectReason.{DisconnectReason}") + + "\n\n" + + TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation); + + private LocalizedString ServerMessage + => TextManager.Get($"ServerMessage.{DisconnectReason}"); + + public LocalizedString PopupMessage + => DisconnectReason switch + { + DisconnectReason.Banned => MsgWithReason, + DisconnectReason.Kicked => MsgWithReason, + DisconnectReason.InvalidVersion => TextManager.GetWithVariables("DisconnectMessage.InvalidVersion", + ("[version]", AdditionalInformation), + ("[clientversion]", GameMain.Version.ToString())), + DisconnectReason.ExcessiveDesyncOldEvent => ServerMessage, + DisconnectReason.ExcessiveDesyncRemovedEvent => ServerMessage, + DisconnectReason.SyncTimeout => ServerMessage, + _ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost")) + }; + + public LocalizedString ReconnectMessage + => PopupMessage + "\n\n" + TextManager.Get("ConnectionLostReconnecting"); + + public PlayerConnectionChangeType ConnectionChangeType + => DisconnectReason switch + { + DisconnectReason.Banned => PlayerConnectionChangeType.Banned, + DisconnectReason.Kicked => PlayerConnectionChangeType.Kicked, + _ => PlayerConnectionChangeType.Disconnected + }; + + public bool ShouldAttemptReconnect + => DisconnectReason + is DisconnectReason.ExcessiveDesyncOldEvent + or DisconnectReason.ExcessiveDesyncRemovedEvent + or DisconnectReason.Timeout + or DisconnectReason.SyncTimeout + or DisconnectReason.SteamP2PTimeOut; + + public bool IsEventSyncError + => DisconnectReason + is DisconnectReason.ExcessiveDesyncOldEvent + or DisconnectReason.ExcessiveDesyncRemovedEvent + or DisconnectReason.SyncTimeout; + + public bool ShouldCreateAnalyticsEvent + => DisconnectReason is not ( + DisconnectReason.Disconnected + or DisconnectReason.Banned + or DisconnectReason.Kicked + or DisconnectReason.TooManyFailedLogins + or DisconnectReason.InvalidVersion); + + private const string lidgrenSeparator = ":hankey:"; + + /// + /// This exists because Lidgren is a piece of shit and + /// doesn't readily support sending anything other than + /// a string through a disconnect packet, so this thing + /// needs a sufficiently nasty string representation that + /// can be decoded with some certainty that it won't get + /// mangled by user input. + /// + public string ToLidgrenStringRepresentation() + { + static string strToBase64(string str) + => Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); + + return DisconnectReason + + lidgrenSeparator + + strToBase64(AdditionalInformation); + } + + public static Option FromLidgrenStringRepresentation(string str) + { + // Lidgren has some hardcoded disconnect strings that it uses + // when it detects that a connection has failed. We can handle + // timeouts, so let's look for strings related to that and return + // an appropriate PeerDisconnectPacket. + switch (str) + { + case Lidgren.Network.NetConnection.NoResponseMessage: + case "Connection timed out": + case "Reconnecting": + return Option.Some(WithReason(DisconnectReason.Timeout)); + } + + static string base64ToStr(string base64) + => Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + + string[] split = str.Split(lidgrenSeparator); + if (split.Length != 2) { return Option.None(); } + if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None(); } + return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1]))); + } + + public static PeerDisconnectPacket Custom(string customMessage) + => new PeerDisconnectPacket( + DisconnectReason.Unknown, + customMessage); + + public static PeerDisconnectPacket WithReason(DisconnectReason disconnectReason) + => new PeerDisconnectPacket(disconnectReason); + + public static PeerDisconnectPacket Kicked(string? msg) + => new PeerDisconnectPacket(DisconnectReason.Kicked, msg ?? ""); + + public static PeerDisconnectPacket Banned(string? msg) + => new PeerDisconnectPacket(DisconnectReason.Banned, msg ?? ""); + + public static PeerDisconnectPacket InvalidVersion() + => new PeerDisconnectPacket( + DisconnectReason.InvalidVersion, + GameMain.Version.ToString()); + + public static PeerDisconnectPacket SteamP2PError(Steamworks.P2PSessionError error) + => new PeerDisconnectPacket( + DisconnectReason.SteamP2PError, + error.ToString()); + + public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error) + => new PeerDisconnectPacket( + DisconnectReason.SteamAuthenticationFailed, + $"{nameof(Steamworks.BeginAuthResult)}.{error}"); + + public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error) + => new PeerDisconnectPacket( + DisconnectReason.SteamAuthenticationFailed, + $"{nameof(Steamworks.AuthResponse)}.{error}"); + } + + // ReSharper disable MemberCanBePrivate.Global, FieldCanBeMadeReadOnly.Global, UnassignedField.Global + public sealed class ServerContentPackage : INetSerializableStruct + { + [NetworkSerialize] + public string Name = ""; + + [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] + public byte[] HashBytes = Array.Empty(); + + [NetworkSerialize] + public string UgcId = ""; + + [NetworkSerialize] + public uint InstallTimeDiffInSeconds; + + [NetworkSerialize] + public bool IsMandatory; + + private Md5Hash? cachedHash; + private DateTime? cachedDateTime; + + public Md5Hash Hash + { + get => cachedHash ??= Md5Hash.BytesAsHash(HashBytes); + set + { + cachedHash = value; + HashBytes = value.ByteRepresentation; + } + } + + public DateTime InstallTime => cachedDateTime ??= DateTime.UtcNow + TimeSpan.FromSeconds(InstallTimeDiffInSeconds); + public RegularPackage? RegularPackage => ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); + public CorePackage? CorePackage => ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); + public ContentPackage? ContentPackage => (ContentPackage?)RegularPackage ?? CorePackage; + + public ServerContentPackage() { } + + public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime) + { + Name = contentPackage.Name; + Hash = contentPackage.Hash; + UgcId = contentPackage.UgcId.TryUnwrap(out var ugcId) + ? ugcId.StringRepresentation + : ""; + IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile); + InstallTimeDiffInSeconds = + contentPackage.InstallTime.TryUnwrap(out var installTime) + ? (uint)(installTime - referenceTime).TotalSeconds + : 0; + } + + public string GetPackageStr() => $"\"{Name}\" (hash {Hash.ShortRepresentation})"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 7cca2d69c..1c0680fbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -11,6 +11,11 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { + /// + /// How much skills drop towards the job's default skill levels when dying + /// + const float SkillReductionOnDeath = 0.75f; + public enum State { Waiting, @@ -285,6 +290,7 @@ namespace Barotrauma.Networking #endif } } + respawnItems.Clear(); foreach (Structure wall in Structure.WallList) { @@ -343,10 +349,14 @@ namespace Barotrauma.Networking RespawnCharactersProjSpecific(shuttlePos); } + public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() + { + return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); + } + public static Affliction GetRespawnPenaltyAffliction() { - var respawnPenaltyAffliction = AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); - return respawnPenaltyAffliction?.Instantiate(10.0f); + return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); } public static void GiveRespawnPenaltyAffliction(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index efbf0683e..9b05b697e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -71,27 +71,18 @@ namespace Barotrauma.Networking public class SavedClientPermission { - public readonly string EndPoint; - public readonly ulong SteamID; + public readonly Either AddressOrAccountId; public readonly string Name; - public HashSet PermittedCommands; + public readonly ImmutableHashSet PermittedCommands; - public ClientPermissions Permissions; + public readonly ClientPermissions Permissions; - public SavedClientPermission(string name, string endpoint, ClientPermissions permissions, HashSet permittedCommands) + public SavedClientPermission(string name, Either addressOrAccountId, ClientPermissions permissions, IEnumerable permittedCommands) { this.Name = name; - this.EndPoint = endpoint; + this.AddressOrAccountId = addressOrAccountId; this.Permissions = permissions; - this.PermittedCommands = permittedCommands; - } - public SavedClientPermission(string name, ulong steamID, ClientPermissions permissions, HashSet permittedCommands) - { - this.Name = name; - this.SteamID = steamID; - - this.Permissions = permissions; - this.PermittedCommands = permittedCommands; + this.PermittedCommands = permittedCommands.ToImmutableHashSet(); } } @@ -219,48 +210,48 @@ namespace Barotrauma.Networking { case "float": msg.WriteVariableUInt32(4); - msg.Write((float)overrideValue); + msg.WriteSingle((float)overrideValue); break; case "int": msg.WriteVariableUInt32(4); - msg.Write((int)overrideValue); + msg.WriteInt32((int)overrideValue); break; case "vector2": msg.WriteVariableUInt32(8); - msg.Write(((Vector2)overrideValue).X); - msg.Write(((Vector2)overrideValue).Y); + msg.WriteSingle(((Vector2)overrideValue).X); + msg.WriteSingle(((Vector2)overrideValue).Y); break; case "vector3": msg.WriteVariableUInt32(12); - msg.Write(((Vector3)overrideValue).X); - msg.Write(((Vector3)overrideValue).Y); - msg.Write(((Vector3)overrideValue).Z); + msg.WriteSingle(((Vector3)overrideValue).X); + msg.WriteSingle(((Vector3)overrideValue).Y); + msg.WriteSingle(((Vector3)overrideValue).Z); break; case "vector4": msg.WriteVariableUInt32(16); - msg.Write(((Vector4)overrideValue).X); - msg.Write(((Vector4)overrideValue).Y); - msg.Write(((Vector4)overrideValue).Z); - msg.Write(((Vector4)overrideValue).W); + msg.WriteSingle(((Vector4)overrideValue).X); + msg.WriteSingle(((Vector4)overrideValue).Y); + msg.WriteSingle(((Vector4)overrideValue).Z); + msg.WriteSingle(((Vector4)overrideValue).W); break; case "color": msg.WriteVariableUInt32(4); - msg.Write(((Color)overrideValue).R); - msg.Write(((Color)overrideValue).G); - msg.Write(((Color)overrideValue).B); - msg.Write(((Color)overrideValue).A); + msg.WriteByte(((Color)overrideValue).R); + msg.WriteByte(((Color)overrideValue).G); + msg.WriteByte(((Color)overrideValue).B); + msg.WriteByte(((Color)overrideValue).A); break; case "rectangle": msg.WriteVariableUInt32(16); - msg.Write(((Rectangle)overrideValue).X); - msg.Write(((Rectangle)overrideValue).Y); - msg.Write(((Rectangle)overrideValue).Width); - msg.Write(((Rectangle)overrideValue).Height); + msg.WriteInt32(((Rectangle)overrideValue).X); + msg.WriteInt32(((Rectangle)overrideValue).Y); + msg.WriteInt32(((Rectangle)overrideValue).Width); + msg.WriteInt32(((Rectangle)overrideValue).Height); break; default: string strVal = overrideValue.ToString(); - msg.Write(strVal); + msg.WriteString(strVal); break; } } @@ -280,7 +271,6 @@ namespace Barotrauma.Networking { ServerLog = new ServerLog(serverName); - Whitelist = new WhiteList(); BanList = new BanList(); ExtraCargo = new Dictionary(); @@ -402,13 +392,12 @@ namespace Barotrauma.Networking public List ClientPermissions { get; private set; } = new List(); - public WhiteList Whitelist { get; private set; } - + private int tickRate = 20; [Serialize(20, IsPropertySaveable.Yes)] public int TickRate { - get; - set; + get { return tickRate; } + set { tickRate = MathHelper.Clamp(value, 1, 60); } } [Serialize(true, IsPropertySaveable.Yes)] @@ -566,7 +555,7 @@ namespace Barotrauma.Networking public bool HasPassword { - get { return password != null; } + get { return !string.IsNullOrEmpty(password); } #if CLIENT set { @@ -814,6 +803,13 @@ namespace Barotrauma.Networking private set; } + [Serialize(120.0f, IsPropertySaveable.Yes)] + public float DisallowKickVoteTime + { + get; + private set; + } + [Serialize(300.0f, IsPropertySaveable.Yes)] public float KillDisconnectedTime { @@ -962,14 +958,7 @@ namespace Barotrauma.Networking public void SetPassword(string password) { - if (string.IsNullOrEmpty(password)) - { - this.password = null; - } - else - { - this.password = password; - } + this.password = string.IsNullOrEmpty(password) ? null : password; } public static byte[] SaltPassword(byte[] password, int salt) @@ -986,14 +975,9 @@ namespace Barotrauma.Networking public bool IsPasswordCorrect(byte[] input, int salt) { - if (!HasPassword) return true; + if (!HasPassword) { return true; } byte[] saltedPw = SaltPassword(Encoding.UTF8.GetBytes(password), salt); - if (input.Length != saltedPw.Length) return false; - for (int i = 0; i < input.Length; i++) - { - if (input[i] != saltedPw[i]) return false; - } - return true; + return saltedPw.SequenceEqual(input); } /// @@ -1048,7 +1032,7 @@ namespace Barotrauma.Networking msg.WriteVariableUInt32((uint)monsterNames.Count); foreach (Identifier s in monsterNames) { - msg.Write(monsterEnabled[s]); + msg.WriteBoolean(monsterEnabled[s]); } msg.WritePadBits(); } @@ -1080,15 +1064,15 @@ namespace Barotrauma.Networking { if (ExtraCargo == null) { - msg.Write((UInt32)0); + msg.WriteUInt32((UInt32)0); return; } - msg.Write((UInt32)ExtraCargo.Count); + msg.WriteUInt32((UInt32)ExtraCargo.Count); foreach (KeyValuePair kvp in ExtraCargo) { - msg.Write(kvp.Key.Identifier); - msg.Write((byte)kvp.Value); + msg.WriteIdentifier(kvp.Key.Identifier); + msg.WriteByte((byte)kvp.Value); } } @@ -1118,7 +1102,7 @@ namespace Barotrauma.Networking msg.WriteVariableUInt32((uint)HiddenSubs.Count); foreach (string submarineName in HiddenSubs) { - msg.Write((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase))); + msg.WriteUInt16((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase))); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs index 7c24c7f02..beafb895a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Xna.Framework; namespace Barotrauma.Networking { - - public class VoipQueue : IDisposable + class VoipQueue : IDisposable { public const int BUFFER_COUNT = 8; protected int[] bufferLengths; @@ -123,16 +119,16 @@ namespace Barotrauma.Networking { if (!CanSend) { throw new Exception("Called Write on a VoipQueue not set up for sending"); } - msg.Write((UInt16)LatestBufferID); - msg.Write(ForceLocal); msg.WritePadBits(); + msg.WriteUInt16((UInt16)LatestBufferID); + msg.WriteBoolean(ForceLocal); msg.WritePadBits(); lock (buffers) { for (int i = 0; i < BUFFER_COUNT; i++) { int index = (newestBufferInd + i + 1) % BUFFER_COUNT; - msg.Write((byte)bufferLengths[index]); - msg.Write(buffers[index], 0, bufferLengths[index]); + msg.WriteByte((byte)bufferLengths[index]); + msg.WriteBytes(buffers[index], 0, bufferLengths[index]); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 5fd149b62..594171239 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; - private IReadOnlyDictionary GetVoteCounts(VoteType voteType, List voters) + private IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) { Dictionary voteList = new Dictionary(); @@ -56,23 +56,5 @@ namespace Barotrauma return selected; } - - public void ResetVotes(List connectedClients) - { - foreach (Client client in connectedClients) - { - client.ResetVotes(); - } -#if CLIENT - foreach (VoteType voteType in Enum.GetValues(typeof(VoteType))) - { - SetVoteCountYes(voteType, 0); - SetVoteCountNo(voteType, 0); - SetVoteCountMax(voteType, 0); - } - UpdateVoteTexts(connectedClients, VoteType.Mode); - UpdateVoteTexts(connectedClients, VoteType.Sub); -#endif - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs deleted file mode 100644 index cb197f3b9..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; - -namespace Barotrauma.Networking -{ - partial class WhiteListedPlayer - { - public string Name; - public string IP; - - public UInt16 UniqueIdentifier; - } - - partial class WhiteList - { - const string SavePath = "Data/whitelist.txt"; - - private List whitelistedPlayers; - public List WhiteListedPlayers - { - get { return whitelistedPlayers; } - } - - public bool Enabled; - - partial void InitProjSpecific(); - public WhiteList() - { - Enabled = false; - whitelistedPlayers = new List(); - - InitProjSpecific(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index 79e8f0e01..69232df45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -19,7 +19,9 @@ namespace Barotrauma public static float DisplayToRealWorldRatio = 1.0f / 100.0f; - public const float DisplayToSimRation = 100.0f; + public const float DisplayToSimRation = 100.0f; + + public const float NeutralDensity = 10.0f; public static bool TryParseCollisionCategory(string categoryName, out Category category) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 95ce1d5c7..ee9831e4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -369,7 +369,7 @@ namespace Barotrauma float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; - density = 10; + density = Physics.NeutralDensity; CreateBody(width, height, radius, density, BodyType.Dynamic, Physics.CollisionCharacter, Physics.CollisionWall | Physics.CollisionLevel, @@ -417,7 +417,7 @@ namespace Barotrauma float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; - density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", 10.0f), MinDensity); + density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); _collisionCategories = collisionCategory; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs index c86e6050c..167725951 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; namespace Barotrauma { - public abstract class Prefab : IDisposable + public abstract class Prefab { public readonly static ImmutableHashSet Types; static Prefab() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 5c3a16671..93fa4d6a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -37,6 +37,14 @@ namespace Barotrauma OnRemoveOverrideFile = onRemoveOverrideFile; } + /// + /// Constructor with only the OnSort callback provided. + /// + public PrefabCollection(Action? onSort) : this() + { + OnSort = onSort; + } + /// /// Method to be called when calling Add(T prefab, bool override). /// If provided, the method is called only if Add succeeds. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs index 631543c0a..8a0bbb7b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs @@ -4,16 +4,20 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; +using Barotrauma.Threading; namespace Barotrauma { public class PrefabSelector : IEnumerable where T : notnull, Prefab { + private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(); + public T? BasePrefab { get { - lock (overrides) { return basePrefabInternal; } + using (new ReadLock(rwl)) { return basePrefabInternal; } } } @@ -21,51 +25,70 @@ namespace Barotrauma { get { - lock (overrides) { return activePrefabInternal; } + using (new ReadLock(rwl)) { return activePrefabInternal; } } } public void Add(T prefab, bool isOverride) { - lock (overrides) { AddInternal(prefab, isOverride); } + using (new WriteLock(rwl)) { AddInternal(prefab, isOverride); } } public void RemoveIfContains(T prefab) { - lock (overrides) { RemoveIfContainsInternal(prefab); } + using (new WriteLock(rwl)) { RemoveIfContainsInternal(prefab); } } public void Remove(T prefab) { - lock (overrides) { RemoveInternal(prefab); } + using (new WriteLock(rwl)) { RemoveInternal(prefab); } } public void RemoveByFile(ContentFile file, Action? callback = null) { - lock (overrides) { RemoveByFileInternal(file, callback); } + var removed = new List(); + using (new WriteLock(rwl)) + { + for (int i = overrides.Count-1; i >= 0; i--) + { + var prefab = overrides[i]; + if (prefab.ContentFile == file) + { + RemoveInternal(prefab); + removed.Add(prefab); + } + } + + if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) + { + RemoveInternal(basePrefabInternal); + removed.Add(p); + } + } + if (callback != null) { removed.ForEach(callback); } } public void Sort() { - lock (overrides) { SortInternal(); } + using (new WriteLock(rwl)) { SortInternal(); } } public bool IsEmpty { get { - lock (overrides) { return isEmptyInternal; } + using (new ReadLock(rwl)) { return isEmptyInternal; } } } public bool Contains(T prefab) { - lock (overrides) { return ContainsInternal(prefab); } + using (new ReadLock(rwl)) { return ContainsInternal(prefab); } } public bool IsOverride(T prefab) { - lock (overrides) { return IsOverrideInternal(prefab); } + using (new ReadLock(rwl)) { return IsOverrideInternal(prefab); } } @@ -73,7 +96,7 @@ namespace Barotrauma private T? basePrefabInternal; private readonly List overrides = new List(); - private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal; + private T? activePrefabInternal => overrides.Count > 0 ? overrides.First() : basePrefabInternal; private void AddInternal(T prefab, bool isOverride) { @@ -84,7 +107,7 @@ namespace Barotrauma } else { - if (BasePrefab != null) + if (basePrefabInternal != null) { string prefabName = prefab is MapEntityPrefab mapEntityPrefab @@ -92,7 +115,7 @@ namespace Barotrauma : $"\"{prefab.Identifier}\""; throw new InvalidOperationException( $"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): " - + $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); + + $"a prefab with the same identifier from \"{activePrefabInternal!.ContentPackage?.Name ?? "[NULL]"}\" ({activePrefabInternal!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); } basePrefabInternal = prefab; } @@ -114,31 +137,12 @@ namespace Barotrauma SortInternal(); } - private void RemoveByFileInternal(ContentFile file, Action? callback) - { - for (int i = overrides.Count-1; i >= 0; i--) - { - var prefab = overrides[i]; - if (prefab.ContentFile == file) - { - RemoveInternal(prefab); - callback?.Invoke(prefab); - } - } - - if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) - { - RemoveInternal(basePrefabInternal); - callback?.Invoke(p); - } - } - private void SortInternal() { overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue)); } - private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any(); + private bool isEmptyInternal => basePrefabInternal is null && overrides.Count == 0; private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab); @@ -153,7 +157,7 @@ namespace Barotrauma { T? basePrefab; ImmutableArray overrideClone; - lock (overrides) + using (new ReadLock(rwl)) { basePrefab = basePrefabInternal; overrideClone = overrides.ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 1060478f1..53d5551b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -87,10 +87,12 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); +#if !DEBUG if (GameMain.GameSession?.GameMode is TestGameMode) { DebugConsole.DeactivateCheats(); } +#endif #endif } /// @@ -167,9 +169,9 @@ namespace Barotrauma if (Character.Controlled != null) { - if (Character.Controlled.SelectedConstruction != null && Character.Controlled.CanInteractWith(Character.Controlled.SelectedConstruction)) + if (Character.Controlled.SelectedItem != null && Character.Controlled.CanInteractWith(Character.Controlled.SelectedItem)) { - Character.Controlled.SelectedConstruction.UpdateHUD(cam, Character.Controlled, (float)deltaTime); + Character.Controlled.SelectedItem.UpdateHUD(cam, Character.Controlled, (float)deltaTime); } if (Character.Controlled.Inventory != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 869d4ccff..86cce0a43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -19,6 +19,7 @@ { Selected.Deselect(); #if CLIENT + GameMain.ParticleManager.ClearParticles(); GUIContextMenu.CurrentContextMenu = null; GUI.ClearCursorWait(); //make sure any textbox in the previously selected screen doesn't stay selected diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 0cae07ba2..c78758bf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -508,7 +508,14 @@ namespace Barotrauma try { - return (float)PropertyInfo.GetValue(parentObject, null); + if (PropertyType == typeof(int)) + { + return (int)PropertyInfo.GetValue(parentObject, null); + } + else + { + return (float)PropertyInfo.GetValue(parentObject, null); + } } catch (TargetInvocationException e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 89c8bc1bd..16e5022f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -308,13 +308,32 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } } return floatValue; } + public static bool TryGetAttributeInt(this XElement element, string name, out int result) + { + var attribute = element?.GetAttribute(name); + result = default; + if (attribute == null) { return false; } + + if (int.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var intVal)) + { + result = intVal; + return true; + } + if (float.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal)) + { + result = (int)floatVal; + return true; + } + return false; + } + public static int GetAttributeInt(this XElement element, string name, int defaultValue) { var attribute = element?.GetAttribute(name); @@ -331,7 +350,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } return val; @@ -350,7 +369,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } return val; @@ -369,7 +388,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } return val; @@ -388,26 +407,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); - } - - return val; - } - - public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) - { - var attribute = element?.GetAttribute(name); - if (attribute == null) { return defaultValue; } - - UInt64 val = defaultValue; - - try - { - val = Steam.SteamManager.SteamIDStringToUInt64(attribute.Value); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } return val; @@ -432,7 +432,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } } @@ -457,7 +457,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } } @@ -559,7 +559,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "! ", e); + DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index d13c4975e..e1ec4e598 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -86,13 +86,6 @@ namespace Barotrauma return config; } - public static Config FromFile(string configFile, in Config? fallback = null) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile); - - return FromElement(doc.Root ?? throw new InvalidOperationException("Unable to load config file: XML document is null."), fallback); - } - public static Config FromElement(XElement element, in Config? fallback = null) { Config retVal = fallback ?? GetDefault(); @@ -108,6 +101,7 @@ namespace Barotrauma #if CLIENT retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); + LoadSubEditorImages(element); #endif return retVal; @@ -288,8 +282,11 @@ namespace Barotrauma { InputType.RadioChat, Keys.None }, { InputType.ActiveChat, Keys.T }, { InputType.CrewOrders, Keys.C }, + { InputType.ChatBox, Keys.B }, { InputType.Voice, Keys.V }, + { InputType.RadioVoice, Keys.None }, + { InputType.LocalVoice, Keys.None }, { InputType.ToggleChatMode, Keys.R }, { InputType.Command, MouseButton.MiddleMouse }, { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, @@ -329,36 +326,69 @@ namespace Barotrauma Dictionary bindings = fallback?.Bindings?.ToMutable() ?? defaultBindings.ToMutable(); foreach (InputType inputType in (InputType[])Enum.GetValues(typeof(InputType))) { - if (!bindings.ContainsKey(inputType)) { bindings.Add(inputType, defaultBindings[inputType]); } + if (!bindings.ContainsKey(inputType)) + { + bindings.Add(inputType, defaultBindings[inputType]); + } } + Dictionary savedBindings = new Dictionary(); bool playerConfigContainsNewChatBinds = false; + bool playerConfigContainsRestoredVoipBinds = false; foreach (XElement element in elements) { foreach (XAttribute attribute in element.Attributes()) { if (Enum.TryParse(attribute.Name.LocalName, out InputType result)) { - if (!playerConfigContainsNewChatBinds) - { - playerConfigContainsNewChatBinds = result == InputType.ActiveChat; - } - bindings[result] = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); + playerConfigContainsNewChatBinds |= result == InputType.ActiveChat; + playerConfigContainsRestoredVoipBinds |= result == InputType.RadioVoice; + var keyOrMouse = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); + savedBindings.Add(result, keyOrMouse); + bindings[result] = keyOrMouse; } } } + // Check for duplicate binds when introducing new binds + foreach (var defaultBinding in defaultBindings) + { + if (!IsSetToNone(defaultBinding.Value) && !savedBindings.ContainsKey(defaultBinding.Key)) + { + foreach (var savedBinding in savedBindings) + { + if (savedBinding.Value == defaultBinding.Value) + { + OnGameMainHasLoaded += () => + { + (string, string)[] replacements = + { + ("[defaultbind]", $"\"{TextManager.Get($"inputtype.{defaultBinding.Key}")}\""), + ("[savedbind]", $"\"{TextManager.Get($"inputtype.{savedBinding.Key}")}\""), + ("[key]", $"\"{defaultBinding.Value.Name}\"") + }; + new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariables("duplicatebindwarning", replacements)); + }; + break; + } + } + } + + static bool IsSetToNone(KeyOrMouse keyOrMouse) => keyOrMouse == Keys.None && keyOrMouse == MouseButton.None; + } + // Clear the old chat binds for configs saved before the introduction of the new chat binds if (!playerConfigContainsNewChatBinds) { - if (bindings.ContainsKey(InputType.Chat)) - { - bindings[InputType.Chat] = Keys.None; - } - if (bindings.ContainsKey(InputType.RadioChat)) - { - bindings[InputType.RadioChat] = Keys.None; - } + bindings[InputType.Chat] = Keys.None; + bindings[InputType.RadioChat] = Keys.None; + } + + // Clear old VOIP binds to make sure we have no overlapping binds + if (!playerConfigContainsRestoredVoipBinds) + { + bindings[InputType.LocalVoice] = Keys.None; + bindings[InputType.RadioVoice] = Keys.None; } Bindings = bindings.ToImmutableDictionary(); @@ -440,6 +470,10 @@ namespace Barotrauma private static Config currentConfig; public static ref readonly Config CurrentConfig => ref currentConfig; +#if CLIENT + public static Action? OnGameMainHasLoaded; +#endif + public static void Init() { XDocument? currentConfigDoc = null; @@ -511,6 +545,7 @@ namespace Barotrauma if (hudScaleChanged) { HUDLayoutSettings.CreateAreas(); + GameMain.GameSession?.HUDScaleChanged(); } GameMain.SoundManager?.ApplySettings(); @@ -571,6 +606,8 @@ namespace Barotrauma .Select(kvp => new XAttribute($"slot{kvp.Index.ToString(CultureInfo.InvariantCulture)}", kvp.Bind.ToString()))); root.Add(inventoryKeyMappingElement); + + SubEditorScreen.ImageManager.Save(root); #endif configDoc.SaveSafe(PlayerConfigPath); @@ -597,5 +634,18 @@ namespace Barotrauma "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); } } + +#if CLIENT + private static void LoadSubEditorImages(XElement configElement) + { + XElement? element = configElement?.Element("editorimages"); + if (element == null) + { + SubEditorScreen.ImageManager.Clear(alsoPending: true); + return; + } + SubEditorScreen.ImageManager.Load(element); + } +#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 94e6c7bef..6435eebf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -77,13 +77,13 @@ namespace Barotrauma Projectile projectile = (entity as Item)?.GetComponent(); if (projectile == null) { - DebugConsole.ShowError("Non-projectile using a delaytype of reachcursor"); + DebugConsole.LogError("Non-projectile using a delaytype of reachcursor"); return; } if (projectile.User == null) { - DebugConsole.ShowError("Projectile: '" + projectile.Name + "' missing user to determine distance"); + DebugConsole.LogError("Projectile: '" + projectile.Name + "' missing user to determine distance"); return; } @@ -129,7 +129,7 @@ namespace Barotrauma if (projectile == null) { #if DEBUG - DebugConsole.ShowError("Non-projectile using a delaytype of reachcursor"); + DebugConsole.LogError("Non-projectile using a delaytype of reachcursor"); #endif return; } @@ -137,7 +137,7 @@ namespace Barotrauma if (projectile.User == null) { #if DEBUG - DebugConsole.ShowError("Projectile " + projectile.Name + "missing user"); + DebugConsole.LogError("Projectile " + projectile.Name + "missing user"); #endif return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 033308a43..891944747 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -158,11 +157,11 @@ namespace Barotrauma /// public readonly bool SpawnIfCantBeContained; public readonly float Impulse; - public readonly float Rotation; + public readonly float RotationRad; public readonly int Count; public readonly float Spread; public readonly SpawnRotationType RotationType; - public readonly float AimSpread; + public readonly float AimSpreadRad; public readonly bool Equip; public readonly float Condition; @@ -198,14 +197,14 @@ namespace Barotrauma SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); - Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("speed", 0.0f)); + Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("launchimpulse", element.GetAttributeFloat("speed", 0.0f))); Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); - Rotation = element.GetAttributeFloat("rotation", 0.0f); + RotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f)); Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); - AimSpread = element.GetAttributeFloat("aimspread", 0f); + AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f)); Equip = element.GetAttributeBool("equip", false); string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); @@ -213,7 +212,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); } - string rotationTypeStr = element.GetAttributeString("rotationtype", Rotation != 0 ? "Fixed" : "Target"); + string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target"); if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) { DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); @@ -264,12 +263,36 @@ namespace Barotrauma public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } + [Serialize(false, IsPropertySaveable.No)] + public bool TransferBuffs { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool TransferAfflictions { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool TransferInventory { get; private set; } + [Serialize("", IsPropertySaveable.No)] public Identifier SpeciesName { get; private set; } [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } + [Serialize(0, IsPropertySaveable.No)] + public int Stun { get; private set; } + + [Serialize("", IsPropertySaveable.No)] + public Identifier AfflictionOnSpawn { get; private set; } + + [Serialize(1, IsPropertySaveable.No)] + public int AfflictionStrength { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool TransferControl { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool RemovePreviousCharacter { get; private set; } + [Serialize(0f, IsPropertySaveable.No)] public float Spread { get; private set; } @@ -1548,24 +1571,23 @@ namespace Barotrauma { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } - if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } - // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well - IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); + if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree characterTalentTree)) { continue; } foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos) { - IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); - if (viableTalents.None()) { continue; } - if (giveTalentInfo.GiveRandom) - { + { + // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well + IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(id => !targetCharacter.Info.UnlockedTalents.Contains(id) && !characterTalentTree.AllTalentIdentifiers.Contains(id)); + if (viableTalents.None()) { continue; } targetCharacter.GiveTalent(viableTalents.GetRandomUnsynced(), true); } else { - foreach (Identifier talent in viableTalents) + foreach (Identifier id in giveTalentInfo.TalentIdentifiers) { - targetCharacter.GiveTalent(talent, true); + if (targetCharacter.Info.UnlockedTalents.Contains(id) || characterTalentTree.AllTalentIdentifiers.Contains(id)) { continue; } + targetCharacter.GiveTalent(id, true); } } } @@ -1629,6 +1651,64 @@ namespace Barotrauma { SwarmBehavior.CreateSwarm(characters.Cast()); } + if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty) + { + if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab)) + { + DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red); + return; + } + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength)); + } + if (characterSpawnInfo.Stun > 0) + { + newCharacter.SetStun(characterSpawnInfo.Stun); + } + foreach (var target in targets) + { + if (!(target is Character character)) { continue; } + if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null) + { + if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; } + for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++) + { + character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null)); + } + } + if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions) + { + foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + { + if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + if (characterSpawnInfo.TransferAfflictions) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + } + } + if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. + { + if (characterSpawnInfo.TransferControl) + { +#if CLIENT + if (Character.Controlled == target) + { + Character.Controlled = newCharacter; + } +#elif SERVER + /*foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Character != target) { continue; } + GameMain.Server.SetClientCharacter(c, newCharacter); + }*/ +#endif + } + if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } + } + } }); } } @@ -1656,92 +1736,101 @@ namespace Barotrauma case ItemSpawnInfo.SpawnPositionType.This: Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { + Item parentItem = entity as Item; Projectile projectile = newItem.GetComponent(); - if (projectile != null && user != null && sourceBody != null && entity != null) + if (entity != null) { var rope = newItem.GetComponent(); - if (rope != null && sourceBody.UserData is Limb sourceLimb) + if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb) { rope.Attach(sourceLimb, newItem); #if SERVER newItem.CreateServerEvent(rope); #endif } - float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread)); - var worldPos = sourceBody.Position; - float rotation = 0; - if (user.Submarine != null) + float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); + float rotation = chosenItemSpawnInfo.RotationRad; + Vector2 worldPos; + if (sourceBody != null) { - worldPos += user.Submarine.Position; + worldPos = sourceBody.Position; + if (user?.Submarine != null) + { + worldPos += user.Submarine.Position; + } + } + else + { + worldPos = entity.WorldPosition; } switch (chosenItemSpawnInfo.RotationType) { case ItemSpawnInfo.SpawnRotationType.Fixed: - rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation); + if (sourceBody != null) + { + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad); + } + else if (parentItem?.body != null) + { + rotation = parentItem.body.TransformRotation(chosenItemSpawnInfo.RotationRad); + } break; case ItemSpawnInfo.SpawnRotationType.Target: rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); break; case ItemSpawnInfo.SpawnRotationType.Limb: - rotation = sourceBody.TransformedRotation; + if (sourceBody != null) + { + rotation = sourceBody.TransformedRotation; + } break; case ItemSpawnInfo.SpawnRotationType.Collider: - rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; + if (parentItem?.body != null) + { + rotation = parentItem.body.Rotation; + } + else if (user != null) + { + rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; + } break; case ItemSpawnInfo.SpawnRotationType.MainLimb: - rotation = user.AnimController.MainLimb.body.TransformedRotation; + if (user != null) + { + rotation = user.AnimController.MainLimb.body.TransformedRotation; + } break; case ItemSpawnInfo.SpawnRotationType.Random: - DebugConsole.ShowError("Random rotation is not supported for Projectiles."); + if (projectile != null) + { + DebugConsole.LogError("Random rotation is not supported for Projectiles."); + } + else + { + rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); + } break; default: - throw new NotImplementedException("Projectile spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); + throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); } - rotation += MathHelper.ToRadians(chosenItemSpawnInfo.Rotation * user.AnimController.Dir); - projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); - } - else - { - var body = newItem.body; - if (body != null) + if (user != null) { - float rotation = MathHelper.ToRadians(chosenItemSpawnInfo.Rotation); - switch (chosenItemSpawnInfo.RotationType) - { - case ItemSpawnInfo.SpawnRotationType.Fixed: - if (sourceBody != null) - { - rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation); - } - break; - case ItemSpawnInfo.SpawnRotationType.Limb: - if (sourceBody != null) - { - rotation += sourceBody.Rotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Collider: - if (entity is Character character) - { - rotation += character.AnimController.Collider.Rotation + MathHelper.PiOver2; - } - break; - case ItemSpawnInfo.SpawnRotationType.MainLimb: - if (entity is Character c) - { - rotation = c.AnimController.MainLimb.body.TransformedRotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Random: - rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); - break; - case ItemSpawnInfo.SpawnRotationType.Target: - break; - default: - throw new NotImplementedException("Spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); - } - body.SetTransform(newItem.SimPosition, rotation); - body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Impulse); + rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir; + } + rotation += spread; + if (projectile != null) + { + projectile.Shoot(user, + ConvertUnits.ToSimUnits(worldPos), + ConvertUnits.ToSimUnits(worldPos), + rotation, + ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + } + else if (newItem.body != null) + { + newItem.body.SetTransform(newItem.SimPosition, rotation); + Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); + newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); } } newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; @@ -1930,7 +2019,7 @@ namespace Barotrauma { continue; } - element.Parent.ApplyToProperty(target, property, n, CoroutineManager.UnscaledDeltaTime); + element.Parent.ApplyToProperty(target, property, n, CoroutineManager.DeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) @@ -2033,7 +2122,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { - return affliction.CreateMultiplied(afflictionMultiplier); + return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); } return affliction; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index ca1a02271..f8815e014 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -2,6 +2,7 @@ using Steamworks.Data; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma.Steam { @@ -42,14 +43,14 @@ namespace Barotrauma.Steam InitializeProjectSpecific(); } - public static ulong GetSteamID() + public static Option GetSteamId() { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { - return 0; + return Option.None(); } - return Steamworks.SteamClient.SteamId; + return Option.Some(new SteamId(Steamworks.SteamClient.SteamId)); } public static bool IsFamilyShared() @@ -63,7 +64,7 @@ namespace Barotrauma.Steam { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } - return Steamworks.SteamApps.IsSubscribedFromFamilySharing; + return Steamworks.SteamApps.IsSubscribedFromFreeWeekend; } public static string GetUsername() @@ -249,37 +250,5 @@ namespace Barotrauma.Steam return 0; } - - public static UInt64 SteamIDStringToUInt64(string str) - { - if (string.IsNullOrWhiteSpace(str)) { return 0; } - UInt64 retVal; - if (str.StartsWith("STEAM64_", StringComparison.InvariantCultureIgnoreCase)) { str = str.Substring(8); } - if (UInt64.TryParse(str, out retVal) && retVal > (1 << 52)) { return retVal; } - if (!str.StartsWith("STEAM_", StringComparison.InvariantCultureIgnoreCase)) { return 0; } - string[] split = str.Substring(6).Split(':'); - if (split.Length != 3) { return 0; } - - if (!UInt64.TryParse(split[0], out UInt64 universe)) { return 0; } - if (!UInt64.TryParse(split[1], out UInt64 y)) { return 0; } - if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return 0; } - - UInt64 accountInstance = 1; UInt64 accountType = 1; - - return (universe << 56) | (accountType << 52) | (accountInstance << 32) | (accountNumber << 1) | y; - } - - public static string SteamIDUInt64ToString(UInt64 uint64) - { - UInt64 y = uint64 & 0x1; - UInt64 accountNumber = (uint64 >> 1) & 0x7fffffff; - UInt64 universe = (uint64 >> 56) & 0xff; - - string retVal = "STEAM_" + universe.ToString() + ":" + y.ToString() + ":" + accountNumber.ToString(); - - if (SteamIDStringToUInt64(retVal) != uint64) { return "STEAM64_" + uint64.ToString(); } - - return retVal; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 3c274b2c3..b4aa8a883 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -3,6 +3,7 @@ using Barotrauma.IO; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; @@ -20,6 +21,16 @@ namespace Barotrauma.Steam public const string PreviewImageName = "PreviewImage.png"; public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; + public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId) + { + workshopId = null; + if (!contentPackage.UgcId.TryUnwrap(out var ugcId)) { return false; } + if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return false; } + + workshopId = steamWorkshopId; + return true; + } + public static partial class Workshop { private struct ItemEqualityComparer : IEqualityComparer @@ -110,7 +121,10 @@ namespace Barotrauma.Steam { NukeDownload(workshopItem); var toUninstall - = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + = ContentPackageManager.WorkshopPackages.Where(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId { Value: var itemId } + && itemId == workshopItem.Id) .ToHashSet(); ContentPackageManager.EnabledPackages.DisableMods(toUninstall); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs index cfd7c255b..3ae90e998 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs @@ -6,7 +6,13 @@ namespace Barotrauma public class InputTypeLString : LocalizedString { private readonly LocalizedString nestedStr; - public InputTypeLString(LocalizedString nStr) { nestedStr = nStr; } + private bool useColorHighlight; + + public InputTypeLString(LocalizedString nStr, bool useColorHighlight = false) + { + nestedStr = nStr; + this.useColorHighlight = useColorHighlight; + } protected override bool MustRetrieveValue() { @@ -23,8 +29,14 @@ namespace Barotrauma foreach (InputType? inputType in Enum.GetValues(typeof(InputType))) { if (!inputType.HasValue) { continue; } - cachedValue = cachedValue.Replace($"[{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); - cachedValue = cachedValue.Replace($"[InputType.{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); + + string keyBindText = GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value; + if (useColorHighlight) + { + keyBindText = $"‖color:gui.orange‖{keyBindText}‖end‖"; + } + cachedValue = cachedValue.Replace($"[{inputType}]", keyBindText, StringComparison.OrdinalIgnoreCase); + cachedValue = cachedValue.Replace($"[InputType.{inputType}]", keyBindText, StringComparison.OrdinalIgnoreCase); } #endif UpdateLanguage(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index 7a710827c..f370ddd71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -181,7 +181,7 @@ namespace Barotrauma public static bool operator !=(string? a, RichString? b) => !(a == b); } - public class StripRichTagsLString : LocalizedString + class StripRichTagsLString : LocalizedString { public readonly RichString RichStr; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 2b1722674..78cc321f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; using System.Globalization; +using System.Text.Unicode; namespace Barotrauma { @@ -30,18 +31,22 @@ namespace Barotrauma public static int LanguageVersion { get; private set; } = 0; - private readonly static Regex isCJK = new Regex( - @"\p{IsHangulJamo}|" + - @"\p{IsHiragana}|" + - @"\p{IsKatakana}|" + - @"\p{IsCJKRadicalsSupplement}|" + - @"\p{IsCJKSymbolsandPunctuation}|" + - @"\p{IsEnclosedCJKLettersandMonths}|" + - @"\p{IsCJKCompatibility}|" + - @"\p{IsCJKUnifiedIdeographsExtensionA}|" + - @"\p{IsCJKUnifiedIdeographs}|" + - @"\p{IsHangulSyllables}|" + - @"\p{IsCJKCompatibilityForms}"); + private static readonly ImmutableArray> CjkRanges = new[] + { + UnicodeRanges.HangulJamo, + UnicodeRanges.Hiragana, + UnicodeRanges.Katakana, + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.EnclosedCjkLettersandMonths, + UnicodeRanges.CjkCompatibility, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.HangulSyllables, + UnicodeRanges.CjkCompatibilityForms + }.Select(r => new Range(r.FirstCodePoint, r.FirstCodePoint+r.Length-1)) + .OrderBy(r => r.Start) + .ToImmutableArray(); /// /// Does the string contain symbols from Chinese, Japanese or Korean languages @@ -54,7 +59,24 @@ namespace Barotrauma public static bool IsCJK(string text) { if (string.IsNullOrEmpty(text)) { return false; } - return isCJK.IsMatch(text); + + for (int i = 0; i < text.Length; i++) + { + char chr = text[i]; + for (int j = 0; j < CjkRanges.Length; j++) + { + var range = CjkRanges[j]; + + // If chr < range.Start, we know that it can't + // be in any of the following ranges, so let's + // not even bother checking them + if (chr < range.Start) { break; } + + // This character is in a range, return true + if (range.Contains(chr)) { return true; } + } + } + return false; } /// @@ -262,9 +284,9 @@ namespace Barotrauma string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]"))); } - public static LocalizedString ParseInputTypes(LocalizedString str) + public static LocalizedString ParseInputTypes(LocalizedString str, bool useColorHighlight = false) { - return new InputTypeLString(str); + return new InputTypeLString(str, useColorHighlight); } public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index c193ba8d7..fc64098bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -5,12 +5,11 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; -using Barotrauma.Networking; // ReSharper disable ArrangeThisQualifier namespace Barotrauma { - internal class PropertyReference + internal sealed class PropertyReference { public object? OriginalValue { get; private set; } @@ -38,87 +37,63 @@ namespace Barotrauma /// Calculate the new value of the property /// /// level of the upgrade - /// Optional XElement reference, only used for error logging. /// - public float CalculateUpgrade(int level, XElement? sourceElement = null) + public object CalculateUpgrade(int level) { - if (OriginalValue is float || OriginalValue is int || OriginalValue is double) + switch (OriginalValue) { - var value = (float) OriginalValue; - - if (level == 0) { return value; } - - if (Multiplier[^1] != '%') + case float _: + case int _: + case double _: { - float multiplier = ParseValue(); - switch (Multiplier[0]) - { - case '*': - case 'x': - return value * (multiplier * level); - case '/': - return value / (multiplier * level); - case '-': - return value - (multiplier * level); - case '+': - return value + (multiplier * level); - case '=': - return multiplier; - } + var value = (float) OriginalValue; + return level == 0 ? value : CalculateUpgrade(value, level, Multiplier); } - else + case bool _ when bool.TryParse(Multiplier, out bool result): { - float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Name, sourceElement, upgrade.Prefab.SuppressWarnings); - return ApplyPercentage(value, multiplier, level); + return result; + } + default: + { + DebugConsole.AddWarning($"Original value of \"{Name}\" in the upgrade \"{upgrade.Prefab.Name}\" is not a integer, float, double or boolean but {OriginalValue?.GetType()} with a value of ({OriginalValue}). \n" + + "The value has been assumed to be '0', did you forget a Convert.ChangeType()?"); + break; } - } - else - { - DebugConsole.AddWarning($"Original value of \"{Name}\" in the upgrade \"{upgrade.Prefab.Name}\" is not a integer, float or a double but {OriginalValue?.GetType()} with a value of ({OriginalValue}). \n" + - "The value has been assumed to be '0', did you forget a Convert.ChangeType()?"); } return 0; } - public static float CalculateUpgrade(object originalValue, int level, string Multiplier) + public static float CalculateUpgrade(float value, int level, string multiplier) { - if (originalValue is float || originalValue is int || originalValue is double) + if (multiplier[^1] != '%') { - var value = (float)originalValue; - - if (Multiplier[^1] != '%') - { - float multiplier = 1.0f; - if (Multiplier.Length > 1) - { - if (prefixCharacters.Contains(Multiplier[0])) - { - float.TryParse(Multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out multiplier); - } - } - switch (Multiplier[0]) - { - case '*': - case 'x': - return value * (multiplier * level); - case '/': - return value / (multiplier * level); - case '-': - return value - (multiplier * level); - case '+': - return value + (multiplier * level); - case '=': - return multiplier; - } - } - else - { - float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Identifier.Empty, suppressWarnings: true); - return ApplyPercentage(value, multiplier, level); - } + return CalculateUpgradeFloat(multiplier, value , level); } - return float.NaN; + + return ApplyPercentage(value, UpgradePrefab.ParsePercentage(multiplier, Identifier.Empty, suppressWarnings: true), level); + } + + private static float CalculateUpgradeFloat(string multiplier, float value, int level) + { + float multiplierFloat = ParseValue(multiplier, value); + + switch (multiplier[0]) + { + case '*': + case 'x': + return value * (multiplierFloat * level); + case '/': + return value / (multiplierFloat * level); + case '-': + return value - (multiplierFloat * level); + case '+': + return value + (multiplierFloat * level); + case '=': + return multiplierFloat; + } + + return 0; } /// @@ -133,7 +108,20 @@ namespace Barotrauma { if (savedValue.NameAsIdentifier() == Name) { - OriginalValue = savedValue.GetAttributeFloat("value", 0.0f); + string value = savedValue.GetAttributeString("value", string.Empty); + + if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out float floatValue)) + { + OriginalValue = floatValue; + } + else if (bool.TryParse(value, out bool boolValue)) + { + OriginalValue = boolValue; + } + else + { + OriginalValue = value; + } } } } @@ -155,30 +143,23 @@ namespace Barotrauma return attributes.Select(attribute => new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray(); } - private float ParseValue() + private static float ParseValue(string multiplier, object? originalValue) { - if (Multiplier.Length > 1) + if (multiplier.Length > 1) { - if (prefixCharacters.Contains(Multiplier[0])) + if (prefixCharacters.Contains(multiplier[0])) { - if (float.TryParse(Multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out float value)) { return value; } + if (float.TryParse(multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out float value)) { return value; } - if (OriginalValue is float || OriginalValue is int || OriginalValue is double) { return (float) OriginalValue; } + if (originalValue is float || originalValue is int || originalValue is double) { return (float) originalValue; } } } - if (!upgrade.Prefab.SuppressWarnings) - { - DebugConsole.AddWarning($"Multiplier for {Name} is too short or does not contain proper prefix. \n" + - $"The value should start with {string.Join(",", prefixCharacters)} and contain a floating point value or another property. \n" + - "The value has been assumed to be '1'."); - } - return 1; } } - internal class Upgrade : IDisposable + internal sealed class Upgrade : IDisposable { private ISerializableEntity TargetEntity { get; } @@ -379,10 +360,10 @@ namespace Barotrauma { if (entity.SerializableProperties.TryGetValue(propertyReference.Name, out SerializableProperty? property) && property != null) { - object? originalValue = property!.GetValue(entity); + object? originalValue = property.GetValue(entity); propertyReference.SetOriginalValue(originalValue); - object newValue = Convert.ChangeType(propertyReference.CalculateUpgrade(Level, sourceElement), originalValue.GetType(), NumberFormatInfo.InvariantInfo); - property!.SetValue(entity, newValue); + object newValue = Convert.ChangeType(propertyReference.CalculateUpgrade(Level), originalValue.GetType(), NumberFormatInfo.InvariantInfo); + property.SetValue(entity, newValue); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index bbb727d01..f66cdcfe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -45,11 +45,12 @@ namespace Barotrauma public int GetBuyprice(int level, Location? location = null) { + int maxLevel = Prefab.MaxLevel; + + if (level > maxLevel) { maxLevel = level; } + int price = BasePrice; - for (int i = 1; i <= level; i++) - { - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, i / (float)Prefab.MaxLevel) / 100); - } + price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); return location?.GetAdjustedMechanicalCost(price) ?? price; } } @@ -108,7 +109,7 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly IEnumerable ItemTags; - + public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) { selfItemTags = element.GetAttributeIdentifierArray("items", Array.Empty())?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; @@ -137,13 +138,22 @@ namespace Barotrauma .Select(it => it.Identifier)); } - public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab) + public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab) { - if (IsWallUpgrade) { return false; } + if (upgradePrefab != null && item.Submarine is { Info: var info } && !upgradePrefab.IsApplicable(info)) { return false; } + + bool isStructure = item is Structure; + switch (IsWallUpgrade) + { + case true: + return isStructure; + case false when isStructure: + return false; + } if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return ((MapEntity)item).Prefab.GetAllowedUpgrades().Contains(Identifier) || + return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); } @@ -173,6 +183,83 @@ namespace Barotrauma public override void Dispose() { } } + internal readonly struct UpgradeMaxLevelMod + { + private enum MaxLevelModType + { + Invalid, + Increase, + Set + } + + private readonly Either tierOrClass; + private readonly int value; + private readonly MaxLevelModType type; + + public int GetLevelAfter(int level) => + type switch + { + MaxLevelModType.Invalid => level, + MaxLevelModType.Increase => level + value, + MaxLevelModType.Set => value, + _ => throw new ArgumentOutOfRangeException() + }; + + public bool AppliesTo(SubmarineInfo sub) + { + if (type is MaxLevelModType.Invalid) { return false; } + + if (tierOrClass.TryGet(out int tier)) + { + return sub.Tier == tier; + } + + if (tierOrClass.TryGet(out SubmarineClass subClass)) + { + return sub.SubmarineClass == subClass; + } + + return false; + } + + public UpgradeMaxLevelMod(ContentXElement element) + { + bool isValid = true; + + SubmarineClass subClass = element.GetAttributeEnum("class", SubmarineClass.Undefined); + int tier = element.GetAttributeInt("tier", 0); + if (subClass != SubmarineClass.Undefined) + { + tierOrClass = subClass; + } + else + { + tierOrClass = tier; + } + + string stringValue = element.GetAttributeString("level", null) ?? string.Empty; + value = 0; + + if (string.IsNullOrWhiteSpace(stringValue)) { isValid = false; } + + char firstChar = stringValue[0]; + + if (!int.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) { isValid = false; } + value = intValue; + + if (firstChar.Equals('+') || firstChar.Equals('-')) + { + type = MaxLevelModType.Increase; + } + else + { + type = MaxLevelModType.Set; + } + + if (!isValid) { type = MaxLevelModType.Invalid; } + } + } + internal partial class UpgradePrefab : UpgradeContentPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection( @@ -205,10 +292,19 @@ namespace Barotrauma onRemoveOverrideFile: null ); - public int MaxLevel { get; } + private readonly int maxLevel; + + public int MaxLevel + { + get + { + Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; + return sub is { Info: var info } ? GetMaxLevel(info) : maxLevel; + } + } public LocalizedString Name { get; } - + public LocalizedString Description { get; } public float IncreaseOnTooltip { get; } @@ -232,8 +328,6 @@ namespace Barotrauma public ContentXElement SourceElement { get; } - private bool disposed; - public bool SuppressWarnings { get; } public bool HideInMenus { get; } @@ -243,17 +337,19 @@ namespace Barotrauma public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade); private Dictionary targetProperties { get; } + private readonly ImmutableArray MaxLevelsMods; public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { Name = element.GetAttributeString("name", string.Empty)!; Description = element.GetAttributeString("description", string.Empty)!; - MaxLevel = element.GetAttributeInt("maxlevel", 1); + maxLevel = element.GetAttributeInt("maxlevel", 1); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); SourceElement = element; var targetProperties = new Dictionary(); + var maxLevels = new List(); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); if (!nameIdentifier.IsEmpty) @@ -291,6 +387,11 @@ namespace Barotrauma Price = new UpgradePrice(this, subElement); break; } + case "maxlevel": + { + maxLevels.Add(new UpgradeMaxLevelMod(subElement)); + break; + } #if CLIENT case "decorativesprite": { @@ -321,14 +422,35 @@ namespace Barotrauma #endif this.targetProperties = targetProperties; + MaxLevelsMods = maxLevels.ToImmutableArray(); upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? .ToImmutableHashSet() ?? ImmutableHashSet.Empty; } - public bool IsDisallowed(Item item) + public int GetMaxLevel(SubmarineInfo info) { - return item.DisallowedUpgradeSet.Contains(Identifier) || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier)); + int level = maxLevel; + + foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) + { + if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } + } + + return level; + } + + public bool IsApplicable(SubmarineInfo? info) + { + if (info is null) { return false; } + + return GetMaxLevel(info) > 0; + } + + public bool IsDisallowed(MapEntity item) + { + return item.DisallowedUpgradeSet.Contains(Identifier) + || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier)); } public static UpgradePrefab? Find(Identifier identifier) @@ -398,18 +520,12 @@ namespace Barotrauma public override void Dispose() { - if (!disposed) - { - Prefabs.Remove(this); #if CLIENT - Sprite?.Remove(); - Sprite = null; - DecorativeSprites.ForEach(sprite => sprite.Remove()); - targetProperties.Clear(); + Sprite?.Remove(); + Sprite = null; + DecorativeSprites.ForEach(sprite => sprite.Remove()); + targetProperties.Clear(); #endif - } - - disposed = true; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs index 2f24d0601..740abe381 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs @@ -1,8 +1,9 @@ +#nullable enable using System; namespace Barotrauma { - public abstract class Either + public abstract class Either where T : notnull where U : notnull { public static implicit operator Either(T t) => new EitherT(t); public static implicit operator Either(U u) => new EitherU(u); @@ -15,22 +16,32 @@ namespace Barotrauma public abstract bool TryCast(out V v); - public abstract override string ToString(); + public abstract override string? ToString(); + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public static bool operator ==(Either? a, Either? b) + => a is null ? b is null : a.Equals(b); + + public static bool operator !=(Either? a, Either? b) + => !(a == b); } - public sealed class EitherT : Either + public sealed class EitherT : Either where T : notnull where U : notnull { public readonly T Value; public EitherT(T value) { Value = value; } - public override string ToString() + public override string? ToString() { return Value.ToString(); } public override bool TryGet(out T t) { t = Value; return true; } - public override bool TryGet(out U u) { u = default; return false; } + public override bool TryGet(out U u) { u = default!; return false; } public override bool TryCast(out V v) { @@ -41,24 +52,34 @@ namespace Barotrauma } else { - v = default; + v = default!; return false; } } + + public override bool Equals(object? obj) + => obj switch + { + EitherT other => Value.Equals(other.Value), + T value => Value.Equals(value), + _ => false + }; + + public override int GetHashCode() => Value.GetHashCode(); } - public sealed class EitherU : Either + public sealed class EitherU : Either where T : notnull where U : notnull { public readonly U Value; public EitherU(U value) { Value = value; } - public override string ToString() + public override string? ToString() { return Value.ToString(); } - public override bool TryGet(out T t) { t = default; return false; } + public override bool TryGet(out T t) { t = default!; return false; } public override bool TryGet(out U u) { u = Value; return true; } public override bool TryCast(out V v) @@ -70,9 +91,19 @@ namespace Barotrauma } else { - v = default; + v = default!; return false; } } + + public override bool Equals(object? obj) + => obj switch + { + EitherU other => Value.Equals(other.Value), + U value => Value.Equals(value), + _ => false + }; + + public override int GetHashCode() => Value.GetHashCode(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs deleted file mode 100644 index e9934ec31..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; - -namespace Barotrauma -{ - public static class IPExtensions - { - //TODO: remove? - //workaround for .NET Framework 4.5 bug; presumably fixed in later versions - //see https://stackoverflow.com/questions/23608829/why-does-ipaddress-maptoipv4-throw-argumentoutofrangeexception - public static IPAddress MapToIPv4NoThrow(this IPAddress address) - { - byte[] addressBytes = address.GetAddressBytes(); - - return new IPAddress(addressBytes.Skip(addressBytes.Length - 4).ToArray()); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs index 08631f611..db6e813b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs @@ -5,5 +5,13 @@ namespace Barotrauma private None() { } public static Option Create() => new None(); + + public override Option Fallback(Option fallback) => fallback; + public override T Fallback(T fallback) => fallback; + + public override bool ValueEquals(T value) => false; + + public override string ToString() + => $"None<{typeof(T).Name}>"; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 73c6d6860..3d19c61ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,3 +1,4 @@ +#nullable enable using System; namespace Barotrauma @@ -15,18 +16,18 @@ namespace Barotrauma public bool IsNone() => this is None; public bool IsSome() => this is Some; - public bool TryUnwrap(out T outValue) + public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); + + public bool TryUnwrap(out T1 outValue) where T1 : T { switch (this) { - case Some { Value: var value }: + case Some { Value: T1 value }: outValue = value; return true; - case None _: - outValue = default; - return false; default: - throw new ArgumentOutOfRangeException(); + outValue = default!; + return false; } } @@ -37,5 +38,30 @@ namespace Barotrauma None _ => Option.None(), _ => throw new ArgumentOutOfRangeException() }; + + public abstract Option Fallback(Option fallback); + public abstract T Fallback(T fallback); + + public abstract bool ValueEquals(T value); + + public override bool Equals(object? obj) + => obj switch + { + Some { Value: var value } => this is Some { Value: { } selfValue } && selfValue.Equals(value), + None _ => IsNone(), + T value => this is Some { Value: { } selfValue } && selfValue.Equals(value), + _ => false + }; + + public override int GetHashCode() + => this is Some { Value: { } value } ? value.GetHashCode() : 0; + + public static bool operator ==(Option a, Option b) + => a.Equals(b); + + public static bool operator !=(Option a, Option b) + => !(a == b); + + public abstract override string ToString(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs index fad94a2a7..5fd1dc3b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs @@ -13,5 +13,13 @@ namespace Barotrauma } public static Option Create(T value) => new Some(value); + + public override Option Fallback(Option fallback) => this; + public override T Fallback(T fallback) => Value; + + public override bool ValueEquals(T value) => Value.Equals(value); + + public override string ToString() + => $"Some<{typeof(T).Name}>({Value})"; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index eb17b94a8..f65b53aab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -1,5 +1,7 @@ +#nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -7,14 +9,55 @@ namespace Barotrauma { public static class ReflectionUtils { - private static Type[] cachedNonAbstractTypes; + private static readonly Dictionary> cachedNonAbstractTypes + = new Dictionary>(); + public static IEnumerable GetDerivedNonAbstract() { - if (cachedNonAbstractTypes == null) + Assembly assembly = typeof(T).Assembly; + if (!cachedNonAbstractTypes.ContainsKey(assembly)) { - cachedNonAbstractTypes = Assembly.GetEntryAssembly().GetTypes().Where(t => !t.IsAbstract).ToArray(); + cachedNonAbstractTypes[assembly] = assembly.GetTypes() + .Where(t => !t.IsAbstract).ToImmutableArray(); } - return cachedNonAbstractTypes.Where(t => t.IsSubclassOf(typeof(T))); + return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); + } + + public static Option ParseDerived(TInput input) where TInput : notnull + { + static Option none() => Option.None(); + + var derivedTypes = GetDerivedNonAbstract(); + + Option parseOfType(Type t) + { + //every TBase type is expected to have a method with the following signature: + // public static Option Parse(TInput str) + var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static); + if (parseFunc is null) { return none(); } + + var parameters = parseFunc.GetParameters(); + if (parameters.Length != 1) { return none(); } + + var returnType = parseFunc.ReturnType; + if (!returnType.IsConstructedGenericType) { return none(); } + if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); } + if (returnType.GenericTypeArguments[0] != t) { return none(); } + + //some hacky business to convert from Option to Option when we only know T2 at runtime + static Option convert(Option option) where T2 : TBase + => option.Select(v => (TBase)v); + Func, Option> f = convert; + var genericArgs = f.Method.GetGenericArguments(); + genericArgs[^1] = t; + var constructedConverter = + f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); + + return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) + as Option ?? none(); + } + + return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 651f13975..49b231dfe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +#if CLIENT using Barotrauma.Networking; using Barotrauma.Steam; +#endif namespace Barotrauma.IO { @@ -229,7 +231,19 @@ namespace Barotrauma.IO public static bool IsPathRooted(string path) => System.IO.Path.IsPathRooted(path); - public static IEnumerable GetInvalidFileNameChars() => System.IO.Path.GetInvalidFileNameChars(); + private static readonly ImmutableHashSet invalidFileNameChars = ImmutableHashSet.Create + ( + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + ); + + /// + /// Returns file name characters that are invalid on any of our supported platforms (essentially the list of invalid characters on Windows) + /// + public static ImmutableHashSet GetInvalidFileNameCharsCrossPlatform() => invalidFileNameChars; } public static class Directory diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 6ca456993..8df000e58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -1,15 +1,13 @@ using System; -using System.Collections; using System.Collections.Generic; -using Barotrauma.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; -using Steamworks.Data; -using Color = Microsoft.Xna.Framework.Color; using System.Text.RegularExpressions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -20,7 +18,7 @@ namespace Barotrauma #if OSX //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac - public static string SaveFolder = Path.Combine( + public static readonly string SaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", @@ -29,7 +27,7 @@ namespace Barotrauma #else //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux - public static string SaveFolder = Path.Combine( + public static readonly string SaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Daedalic Entertainment GmbH", "Barotrauma"); @@ -165,15 +163,6 @@ namespace Barotrauma return ownedSubmarines; } - /*public static void LoadMultiplayerCampaignState(string filePath, MultiPlayerCampaign multiplayerCampaign) - { - DebugConsole.Log("Loading save file for an existing game session (" + filePath + ")"); - DecompressToDirectory(filePath, TempPath, null); - XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); - if (doc == null) { return; } - gameSession.Load(doc.Root); - }*/ - public static XDocument LoadGameSessionDoc(string filePath) { DebugConsole.Log("Loading game session doc: " + filePath); @@ -391,73 +380,69 @@ namespace Barotrauma } - public static System.IO.Stream DecompressFiletoStream(string fileName) + public static System.IO.Stream DecompressFileToStream(string fileName) { - using (FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open)) - { - System.IO.MemoryStream decompressedFileStream = new System.IO.MemoryStream(); + using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read); + System.IO.MemoryStream streamToReturn = new System.IO.MemoryStream(); - using (GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress)) - { - decompressionStream.CopyTo(decompressedFileStream); - return decompressedFileStream; - } - } + using GZipStream gzipStream = new GZipStream(originalFileStream, CompressionMode.Decompress); + gzipStream.CopyTo(streamToReturn); + + streamToReturn.Position = 0; + return streamToReturn; } - private static bool DecompressFile(bool writeFile, string sDir, GZipStream zipStream, ProgressDelegate progress, out string fileName) + private static bool IsExtractionPathValid(string rootDir, string fileDir) + { + string getFullPath(string dir) + => (string.IsNullOrEmpty(dir) + ? Directory.GetCurrentDirectory() + : Path.GetFullPath(dir)) + .CleanUpPathCrossPlatform(correctFilenameCase: false); + + string rootDirFull = getFullPath(rootDir); + string fileDirFull = getFullPath(fileDir); + + return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase); + } + + private static bool DecompressFile(bool writeFile, string sDir, System.IO.BinaryReader reader, ProgressDelegate progress, out string fileName) { fileName = null; + if (reader.PeekChar() < 0) { return false; } + //Decompress file name - byte[] bytes = new byte[sizeof(int)]; - int Readed = Read(zipStream, bytes, sizeof(int)); - if (Readed < sizeof(int)) - return false; - - int iNameLen = BitConverter.ToInt32(bytes, 0); - if (iNameLen > 255) + int nameLen = reader.ReadInt32(); + if (nameLen > 255) { - throw new Exception("Failed to decompress \"" + sDir + "\" (file name length > 255). The file may be corrupted."); + throw new Exception( + $"Failed to decompress \"{sDir}\" (file name length > 255). The file may be corrupted."); } - bytes = new byte[sizeof(char)]; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < iNameLen; i++) - { - Read(zipStream, bytes, sizeof(char)); - char c = BitConverter.ToChar(bytes, 0); - sb.Append(c); - } - string sFileName = sb.ToString().Replace('\\', '/'); + byte[] strBytes = reader.ReadBytes(nameLen * sizeof(char)); + string sFileName = Encoding.Unicode.GetString(strBytes) + .Replace('\\', '/'); fileName = sFileName; progress?.Invoke(sFileName); //Decompress file content - bytes = new byte[sizeof(int)]; - Read(zipStream, bytes, sizeof(int)); - int iFileLen = BitConverter.ToInt32(bytes, 0); - - bytes = new byte[iFileLen]; - Read(zipStream, bytes, bytes.Length); + int contentLen = reader.ReadInt32(); + byte[] contentBytes = reader.ReadBytes(contentLen); string sFilePath = Path.Combine(sDir, sFileName); string sFinalDir = Path.GetDirectoryName(sFilePath); - string sDirFull = (string.IsNullOrEmpty(sDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); - string sFinalDirFull = (string.IsNullOrEmpty(sFinalDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sFinalDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); - - if (!sFinalDirFull.StartsWith(sDirFull, StringComparison.OrdinalIgnoreCase)) + if (!IsExtractionPathValid(sDir, sFinalDir)) { throw new InvalidOperationException( $"Error extracting \"{sFileName}\": cannot be extracted to parent directory"); } - + if (!writeFile) { return true; } - if (!Directory.Exists(sFinalDir)) - Directory.CreateDirectory(sFinalDir); + Directory.CreateDirectory(sFinalDir); int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { @@ -465,7 +450,7 @@ namespace Barotrauma { using (FileStream outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) { - outFile.Write(bytes, 0, iFileLen); + outFile.Write(contentBytes, 0, contentLen); } break; } @@ -479,26 +464,6 @@ namespace Barotrauma return true; } - private static int Read(GZipStream zipStream, byte[] bytes, int amount) - { - int read = 0; - - // BUG workaround for .NET6 causing save decompression to fail -#if NET6_0 - for (int i = 0; i < amount; i++) - { - int result = zipStream.ReadByte(); - if (result < 0) { break; } - - bytes[i] = (byte) result; - read++; - } -#else - read = zipStream.Read(bytes, 0, amount); -#endif - return read; - } - public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); @@ -507,9 +472,9 @@ namespace Barotrauma { try { - using (FileStream inFile = File.Open(sCompressedFile, System.IO.FileMode.Open, System.IO.FileAccess.Read)) - using (GZipStream zipStream = new GZipStream(inFile, CompressionMode.Decompress, true)) - while (DecompressFile(true, sDir, zipStream, progress, out _)) { }; + using (var memStream = DecompressFileToStream(sCompressedFile)) + using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) + while (DecompressFile(true, sDir, reader, progress, out _)) { }; break; } @@ -530,12 +495,12 @@ namespace Barotrauma { try { - using FileStream inFile = File.Open(sCompressedFile, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using GZipStream zipStream = new GZipStream(inFile, CompressionMode.Decompress, true); - while (DecompressFile(false, "", zipStream, null, out string fileName)) - { - paths.Add(fileName); - } + using (var memStream = DecompressFileToStream(sCompressedFile)) + using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) + while (DecompressFile(false, "", reader, null, out string fileName)) + { + paths.Add(fileName); + } } catch (System.IO.IOException e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs new file mode 100644 index 000000000..25a2ac7fa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs @@ -0,0 +1,34 @@ +using System.Threading; + +namespace Barotrauma.Threading +{ + internal readonly ref struct ReadLock + { + private readonly ReaderWriterLockSlim rwl; + public ReadLock(ReaderWriterLockSlim rwl) + { + this.rwl = rwl; + rwl.EnterReadLock(); + } + + public void Dispose() + { + rwl.ExitReadLock(); + } + } + + internal readonly ref struct WriteLock + { + private readonly ReaderWriterLockSlim rwl; + public WriteLock(ReaderWriterLockSlim rwl) + { + this.rwl = rwl; + rwl.EnterWriteLock(); + } + + public void Dispose() + { + rwl.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 9922dc37f..1f5b69a16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -3,13 +3,12 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; -using System.Runtime.CompilerServices; namespace Barotrauma { @@ -26,14 +25,14 @@ namespace Barotrauma } } - public static partial class ToolBox + static partial class ToolBox { - static internal class Epoch + internal static class Epoch { private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// - /// Returns the current Unix Epoch (Coordinated Universal Time ) + /// Returns the current Unix Epoch (Coordinated Universal Time) /// public static int NowUTC { @@ -161,7 +160,7 @@ namespace Barotrauma public static string RemoveInvalidFileNameChars(string fileName) { - var invalidChars = Path.GetInvalidFileNameChars().Concat(new char[] {':', ';', '<', '>', '"', '/', '\\', '|', '?', '*'}); + var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(new char[] {';'}); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); @@ -424,7 +423,7 @@ namespace Barotrauma for (int i = 0; i < numberOfBits; i++) { bool bit = originalBuffer.ReadBoolean(); - buffer.Write(bit); + buffer.WriteBoolean(bit); } buffer.BitPosition = 0; @@ -712,5 +711,15 @@ namespace Barotrauma return e; } + + public static void ThrowIfNull([NotNull] T o) + { + if (o is null) { throw new ArgumentNullException(); } + } + + public static string GetFormattedPercentage(float v) + { + return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 335b95f90..49a90c5db 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,313 @@ +--------------------------------------------------------------------------------------------------------- +v0.19.8.0 +--------------------------------------------------------------------------------------------------------- + +- Minor improvements and fixes to the tutorials (e.g. safeguards to prevent dying or getting stuck). +- Fixed Typhon 2's chaingun being placed inside solid walls. +- Fixed Tandem Fire talent causing a crash if there's no allies alive. +- Fixed bots being unable to shoot at ice spires with pulse laser or chaingun. +- Removed outdated Deep Diver loading screen tip. +- Fixed misaligned connection panel interface when repairing a status monitor. +- Fixed Winterhalter battery recharge speed being limited by the relay that supplies power to them, making recharge speed upgrades useless. +- Fixed a networking issue that caused afflictions' periodic effects (e.g. nausea-induced vomiting) to happen too frequently client-side. +- Fixed none of the contextual orders except "wait here" showing the name of the order in the tooltip. +- Fixed inconsistencies in some crawler swarm mission names (large swarms not being described as large). +- Fixed mineral mission resources sometimes spawning in separate caves. +- Fixed minerals mission resources sometimes spawning outside the level. +- Moved handheld sonar's drag icon to a more appropriate position. +- Fixed buoyancy making locked subs slowly move vertically. +- Reactors don't explode if they reach 0 condition without fuel. +- Fixed motion sensor not detecting pets. +- Fixed cargo capacity displayed in the mission selection screen including the cargo containers in the outpost the sub is docked. +- Fixed clients being unable to select the respawn shuttle if they have to download it from the server. +- Fixed duplicate banlist entires when a client gets banned due to an incorrect password. +- Some wire and waypoint cleanup to R-29 and Remora. +- Removed a wall in Remora to allow for better movement inside. +- Changed one railgun shelf to boxes shelf on Remora. +- Added a coilgun and a couple more diving suit cabinets to R-29. +- Fixed "completed initialization before receiving content package order" error when trying to reconnect to a SteamP2P server. + +--------------------------------------------------------------------------------------------------------- +v0.19.7.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Completely remade tutorials. There is now a Basics Tutorial to cover basics like moving, inventory and repairs. More specific tasks are explained in the Roles Tutorial, where every job has their own tutorial to go through, explaining what it means to be e.g. an Engineer or a Captain. +- Added new mining missions, including some in the abyss. +- Reintroduced separate local/radio voice chat keys as a legacy option. Now it's again possible to speak with voice activation by default and use a push-to-talk button for radio, the same way as before, by setting the chat mode to Local and using the new radio voice chat hotkey. +- Device/item UIs can be moved around by dragging. +- Allow using devices while on a ladder or sitting on a chair. +- Changed reactor temperature bar colors (from blue to red). +- Higher quality stun batons cause heavier stun. +- Changed unit load device capacity to 12 (because the sprite has space for 12) and made them waterproof. +- Changed fabricator skill calculations: the most inadequate of the required skills determines the fabrication time (instead of the average). +- Made dying drop a characters' skills towards the maximum initial skill instead of minimum. +- Added a new keybind for opening and closing the chat box. The default bind is B. +- Added a warning if a new keybind overlaps with any of the player's existing binds. +- Overvoltage makes devices perform better, increasing the output of engines, making fabricators, deconstructors and pumps operate faster, electrical discharge coils do more damage, batteries recharge faster and oxygen generators generate more oxygen. Encourages operating the reactor manually and hopefully makes it a little more engaging. +- Added more randomness to junction box overvoltage damage, and made partially damaged boxes take more damage from overvoltage. Prevents all boxes from breaking at the same time, making overvoltage less of a pain to deal with and intentionally overvolting devices more worthwhile. +- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate a new benefit to operating reactors manually. +- Signals no longer set the fission and turbine rates of the reactor instantaneously, making automated reactor circuits less overpowered. They are still viable, but especially now with the addition of the extra incentives for operating the reactor manually, they're no longer as clearly the best and most efficient way to operate the reactor, making manual operation more worthwhile. +- Made the "distort" camera effect a little less obtrusive and glitchy-looking (smoother texture + less heavy effect). +- Made water-sensitive materials (lithium, potassium, sodium) spawn in waterproof chemical crates. +- Made crates deconstruct much faster to make them easier to get rid of. +- Sonar disruptions now hide minerals. +- Grayed out ranged weapons' crosshair when reloading (similar to turret crosshairs). +- Disabled the autodocking prompt (which verifies whether you actually want to dock when docking is initiated by an automated circuit) in single player. +- Improved the way drag is applied on submerged items. Fixes heavy items dropping at unnaturally high speeds in water. +- Added a splash effect when an item falls into water. +- The deconstructor UI shows what the input items deconstruct to (particularly important now with the lossy deconstruction recipes - it can be risky to deconstruct something just to see what materials it gives out if that results in material loss). +- Wall and device repair costs in outposts are calculated based on the amount of damage on your sub, instead of always having a fixed price. +- Inflamed lung doesn't affect characters that don't need oxygen. +- Added swarm behavior for crawler husks. +- Added some more oomph to nuclear explosions. +- Adjust the alpha of the outpost service icons according to distance to make it easier to estimate where the NPC is at. Show the title of the NPC when hovering the cursor over the icon. +- Added "unlockmission" console command. +- Added "setcampaignmetadata" console command (may be useful for modders creating custom scripted events for the campaign). +- Changed how NPC "titles" work. Previously we defined "titles" for the pirates (e.g. "Pirate Lord" and such), and the title replaced the name of the NPC (which made their dialog a little awkward). Now we display both the name and the title over the character, and special outpost NPCs also have titles. +- Gave diving masks to most NPCs. +- Changed the burn overlay formula: now also the non-affected limbs get half of the effect, because the sharp contrast between limbs looked weird. +- Restored the 3-shell Railgun rack as a legacy option. +- Reworded the "respawn with penalty" prompt to make it less confusing: you always get a penalty to your skills when you die now, and Reaper's Tax is an "extra penalty" you get on top of that if you opt to respawn mid-round. The intention behind this is to incur a cost to respawning, as it shouldn't be possible to get unlimited free reinforcements and supplies mid-round. +- Made SIGTERM close the linux server gracefully. +- Made respawn items (suits, scooters) spawn in the respawn shuttle's cabinets when possible. +- Show a healthbar on items (e.g. eggs and thalamus organs) when damaging them with handheld weapons (melee or ranged). + +Submarines: +- Added a new intermediate transport sub, Camel. +- Added submarine tiers. Higher-tier submarines can be upgraded further than lower-tier submarines. +- Overhauled and balanced submarine upgrades. +- Added Large Weapon Hardpoints. +- Added Flak Cannon and Double Coilgun as new Large Weapons. +- Railgun is now considered a Large Weapon. +- Added an upgrade that adds a mineral scanner to nav terminals and sonar monitors.cannon +- Submarine class now affects which upgrades are available for the sub. +- Removed the Deep Diver class: the way we see it, Deep Divers didn't have a clear enough role in the game, especially considering that hull upgrades served pretty much the same purpose. In practice, the only clear benefit of a Deep Diver was being able to get through the very last levels of the campaign, and having to switch to one just for that purpose wasn't fun. Now any submarine with full hull upgrades can get all the way to the end of the campaign. +- Fixed messy wiring in Typhon 2's bottom left hardpoint. +- Winterhalter and Remora are now Scout class ships. +- Added some loose vents and panels to Herja, Winterhalter and Barsuk, fixed invisible "loose panel" (news stand) in Orca 2. +- Fixed floating light component in Orca 2. +- Medical fabricator now consumes 500 power on all submarines, to be consistent with other fabricators. +- Updated prices of all submarines to match tiers. +- Gave Typhon 2 better stats and even more firepower, to outclass the original Typhon. +- Improved R-29's speed and gave it a Flak Cannon. +- Added Large Weapon hardpoints to Berilia to make it a Tier 3 transport. +- Tweaked the hulls and waypoints around Herja's top docking hatch to make it easier for bots to reach and weld. +- Fixed a waypoint/hull issue in Typhon's stowage compartment (waypoint in such a tight space the bots couldn't reach it). + +Balance: +- "Mission cheesing" by repeatedly undocking and redocking to an outpost to reroll the mission events no longer works: new mission events don't reappear until one "world step" has passed (~10 minutes or traversing through one level). +- Balance pass on handheld weapons: adjusted reload times, damages, stun durations, recoil and ammo stack sizes. +- Reduced tools' structure damage (dual-wielded storage containers no longer chew through submarine walls in seconds). +- Increased heavy ruin wall health to make it less easy to cheese your way into the artifact room in ruins. +- Made boomstick fire in bursts of 2 (similar to deadeye carbide) to prevent ridiculous fire rates with quick-reloading. +- Added EMP effect to nuclear depth charges for consistency. +- Pulse Laser and Railgun now have similar power consumption as other turrets. +- Changed how skill levels affect the quality of fabricated items. Previously having a skill level equal to or higher than the item's skill requirement would result in a good quality item, meaning that practically everyone could e.g. fabricate good quality oxygen tanks. Now your skill needs to be >20% from the minimum skill requirement towards 100 (e.g. if the item requires 20 skill to fabricate, 36 results in a higher quality item). +- Reduced PUCS's radiation resistance from 100% to 90%. Complete invulnerability to radiation has way too much potential for exploits and overpowered strategies. +- Adjusted supplies in pirate submarines. +- Turned some weapons' burn damage into explosion damage. +- Made the extra sales from "traveling tradesman" talent stack. +- Terminal ignores empty signals. +- Reduced commonness of molochs (as they can take a lot of time to kill, running into multiple of them can quickly become a chore) +- Removed steel requirement for depth charges. Fabricate decoy depth charges from depth charges, rather than from the base material. +- Reduced the Pulse Laser tri-laser bolt spread. +- Explosions are now calculated differently, using the number of limbs to divide the damage (up to a maximum of 15 limbs). Adjusted explosion damage values to match new calculations. +- Coilgun costs 5000 marks to install, Pulse Laser and Chaingun 6000. Large turrets each cost 7500 each. +- Made mudraptor eggs modestly profitable for farming (decreased cost from shop, increased deconstruction yields). +- Mineral yield and spawn rates rebalance: minerals found are now much more dependent on location (biome, cave, abyss). +- Balanced existing mineral missions: adjusted rewards & required minerals, and required some minerals to be handed over to the outposts as proof of their existence. +- Rebalanced Engine Force values to better match hull size. Most Scouts (Azimuth, Orca2, Remora, Winterhalter) are now faster. Humpback, Typhon and Orca are slightly slower. + +Multiplayer: +- Fixed missions sometimes unlocking in incorrect locations in MP campaign, making them either unselectable or causing a "mission mismatch" error when the round starts. +- Fixed clients downloading submarines they already have from the server if the mods those submarines are in are not currently enabled. +- Significantly sped up file transfers (mods, submarine files, campaign saves). +- Clients who've recently joined (by default 2 minutes) are not allowed to vote to kick others, and vote kicking someone always requires at least 2 votes. +- Servers don't allow selecting hidden jobs (jobs only used by NPCs) as job preferences. +- The minimum kick vote counts are no longer rounded down. Previously if you had for example four players on the server and the minimum vote count set to 60%, kicking would require 2 votes, now it requires 3. +- Fixed inventory and wallet resetting if a campaign round ends when a client's character has spawned, but the client is not currently controlling it (e.g. due to getting kicked to the lobby). +- Fixed spectator checkbox overlapping with the character info if you get kicked to the lobby mid-round. +- Fixed "kick" button staying disabled indefinitely if you vote to kick someone and the vote doesn't go through. +- Fixed Steamworks publish tab showing the "free weekend" message when using Steam family sharing. +- Minor tweaks to the end of PvP missions to make them a little less underwhelming: instead of ending the round immediately when one team is dead (without even giving enough time to see the enemy die), there's a brief delay, a message box and a camera transition to let the players see what happened. +- Fixed PvP team assignment sometimes being wildly imbalanced, even when there were enough players with no preference to make the team sizes equal. +- Fixed clients getting stuck in the loading screen if they happen to disconnect at the right moment between rounds. +- Fixed bank balance not getting corrected if it's become desynced by e.g. client-side commands. +- Fixed server not registering a client's character as disconnected if the client disconnects and reconnects before the round has fully started, causing the client to get stuck as a spectator when they rejoin. +- Fixed clients disabling their client-side-only mods when they join a server. +- Fixed hull/item repairs purchased from an outpost sometimes not getting applied client-side. +- Fixed "missing entity" errors in a specific situation in multiplayer. Occurred when a respawn shuttle was enabled and loaded on the server (= i.e. in a non-outpost level), and a client disconnected and immediately reconnected. This would cause the client to deselect the respawn shuttle and make them start the round without loading one, leading to the "missing entity" issues due to the shuttle only existing server-side. +- Fixed damage visuals not showing on characters who've died off-screen. +- Fixed ability to upgrade the sub when there's a switch pending in multiplayer. +- Fixed friendly fire and karma always showing up as disabled on dedicated servers in the server list. +- Fixed spineling spikes fired by a human with spineling genes not damaging any human characters (enemies in PvP, pirates in pirate missions) when friendly fire is disabled. +- Fixed "invalid ExecuteAttack message: limb index out of bounds" errors when you join a server where a character has fired spineling spikes with spineling genes mid-round. +- Fixed "entity not found" errors if a shuttle or submarine ends up absurdly deep in multiplayer (>100 km). We don't even know how someone managed to pull this off. +- Fixed rapidly clicking on the mission giver sometimes not giving all the available missions when the "Use" input is set to LMB. Happened because the conversation logic didn't check if there's another conversation active, causing the server to show a new conversation when clicking the NPC, without interrupting/continuing the previous conversation. +- Made shockjock event only show for the player triggering the event (making it visible for everyone works kind of weirdly, when the event involves talking to an NPC next to the character who triggered the event). +- Fixed outpost events getting stuck at the last ConversationAction if another client has finished the action. + +Optimization: +- Updated our runtime to .NET 6, which should yield significant performance improvements. Do note that this unfortunately means we'll have to drop support for macOS versions older than 10.15, but we have taken some measures to help the affected Mac players continue having access to Barotrauma. More info here https://store.steampowered.com/news/app/602960/view/3367025204056277713. +- Optimized afflictions that apply other afflictions on the character (e.g. radiation sickness, drunkenness, opiate withdrawal). +- Optimizations to the talent system, particularly when the talent menu is open and when there's a large number of talents (e.g. when using mods that make all talents available to every class). +- Physics optimization: fixed submerged items' physics bodies staying active indefinitely even after they've come to rest due to buoyant forces being applied on them constantly. Now we stop updating bodies that have come to rest on the floor and aren't light enough to float. +- Optimized AI objectives that make bots fetch items (combat, contain item, decontain item, get item). + +Submarine editor: +- Fixed door gaps not appearing in the sub editor until you select the door. +- Fixed sub editor background images not saving. +- Fixed turret lightsource rotation not refreshing in the sub editor when flipping the item. +- Fixed prefab placement breaking in the sub editor if LMB is held while moving the cursor outside of the selection panel. +- Fixed several instances of janky UI interactions in the submarine editor: dragging the selection rectangle now works even if the cursor reaches into the prefab list; letting go of a dragged entity works even if the cursor reaches into the prefab list; the dragged entity no longer goes invisible when reaching into the prefab list. +- Made PowerContainer recharge speed always default to 0. +- Fixed adding resizeable items (like ladders) not being registered in the sub editor's command history, preventing undoing it. +- Changed default reactor output from 10,000 kW to 5000 kW. +- Decreased Winterhalter reactor output and increased its fuel consumption rate. +- Fixed some gap issues in Winterhalter. +- Fixed medics not having access to the toxin cabinet in Barsuk. +- Fixed medic, engineer and mechanic spawnpoints having no tags in Typhon. +- Fixed crashing when trying to multi-edit a string value in the sub editor. +- Fixed dragged objects becoming invisible if you bring the cursor over a UI element in the sub editor. +- Fixed screwdrivers and wires in your "inventory" being included in the total item count in the sub editor's wiring mode. +- Fixed entities that were below the cursor when starting to resize a structure staying highlighted during resizing. +- Fixed sub editor treating the autosave interval as minutes instead of seconds (saving every 300 minutes instead of 300 seconds). + +Fixes: +- Fixed "power flowback" issue in turrets. As of the power rework, wires connected to the same input or output pin of a device are considered to be in the same grid, which in practice meant a turret could be connected to another supercapacitor through the power_in connection of another turret, even if there was no direct connection between the 1st turret and the supercapacitor. Now the turrets (and electrical discharge coils) need to be wired directly to the supercapacitor. +- Fixed brief freezes when monsters spawn mid-round. +- Fixed turrets linked to the same loader messing up the upgrade store UI and causing item swaps to cost more than they should. +- Fixed submarines always saving in the root folder of a local mod, instead of the subfolder they were originally in. +- Fixed Reaper's Tax not stacking. +- Fixes to ruin decals in a bunch of ruin modules. +- Fixed a waypoint issue in the Alien_Entrance3 ruin module. +- Removed oxygen tanks from DockingModule_01_Colony. +- Fixed duct block's misaligned broken sprite. +- Fixed status monitor calculating linked hulls' water amounts incorrectly (displaying the average of their water percentages, which isn't correct if the hulls aren't the same size). +- Fixed inactive components (components not currently sending any signal) not reactivating if their output is set to a non-empty value. +- Fixed missing gap in SecurityModule_02. +- Fixed lack of outpost events in difficulties past 80 (which no longer occur normally but still exist in old saves and mods). +- Fixed lithium and magnesium descriptions. +- Adjusted hulls in DockingModule_02_Colony to prevent bots from jumping off the ledge. +- Fixed motion sensors detecting pets as monsters (pets are now a separate target type). +- Fixed helmets not protecting against concussions. +- Fixed safety harness not protecting against lacerations. +- Fixed increasing an item's HealthMultiplier not increasing the current condition (so e.g. doubling the item's max health would cause it to have 50% condition). +- Fixed successive event dialogs in the same prompt scrolling the prompt back up and then down. +- Fixed missing "pirateclothes" inventory icon. +- Made bots better at figuring out which button controls a door when there's some complex circuit involved. Previously the bots would try to find a button connected to any of the door's connections via wires/circuits, now only the toggle and set_state inputs are considered. +- Bots now clearly prefer using buttons linked to the door in the sub editor. Can be used as another way to help bots figure out which button they should press in situations with multiple buttons and complex door control logic. +- Fixed bots failing to find a path to a couple of spots on Herja. +- Fixed alien materials (physicorium, incendium, fulgurium, dementonite, paralyxis) not being shown on the mineral scanner. +- Another fix to cave generation to prevent it from creating impassable paths. +- Fixed inability to use manual assignment for bot orders with options. +- Fixed all boolean components (And, Or, Xor) using the And Component's tooltip for the "timeframe" property. +- Fixed boolean operator component (And, Or, Xor) timeframes not working correctly in some situations (non-zero timeframe, empty false output). The component would deactivate as soon as it stops sending an output, which could prevent some inputs from timing out (meaning that the component could send a signal again as soon as it receives signal A, even if signal B hasn't been received within the timeframe). +- Fixed PUCS consuming the medical item inside it when a welding fuel or incendium tank is inserted. +- Fixed a level generation issue that sometimes made the level impassable if there happened to be a cave right above the outpost. +- Fixed holes on sloped walls being impossible to pass through when you're swimming straight down/up (or straight right/left depending on the wall): the walls are technically considered either horizontal or vertical (depending on the angle of the slope), and you would have to swim in a direction perpendicular to this "technical" direction of the wall. +- Fixed retrying the Hognose mission making a new Hognose join your crew every time. +- Fixed idling NPCs sometimes getting stuck on ladders. +- Fixed mirrored turrets being displayed backwards on the status monitor. +- Fixed character's hands getting "stuck" if you handcuff yourself while dragging someone. +- Fixed dragged character's arms not being pulled towards you, making it look like you're dragging them without touching if you run or walk away while dragging. +- Fixed dragged bots slowly moving constantly, preventing them from switching to the normal standing pose. +- Fixed bots having trouble fixing leaks in multi-hull rooms: they were required to be in the same hull as the leak, which prevented them from fixing leaks in e.g. R-29's bilge. +- Fixed combat missions not ending the round if both crews are dead. +- Fixed bots stating the name of the character they're firing at with turrets, making it seem like they know the name of every pirate they come across and magically recognize them through the walls of the enemy sub. +- Fixed Chaingun rotation speed not being affected by the weapons skill. +- Fixed crashing when using ':' in item assembly names on Linux platforms. +- Fixed ImmuneToPressure ability flag being ignored on characters who don't need air (in practice meaning that you can get killed by pressure if you get huskified even if you have a talent that makes you immune to pressure). +- Fixed geneticmaterialcrawler_unresearched3 producing mudraptor genes. +- Fixed linked subs still sometimes getting placed on the wrong side of the docking port when switching subs. +- Fixes to ruin door connections, wiring and connection panels. +- Fixed "insurance policy" giving the money to the dead character instead of the bank. +- Fixed damage to mirrored wall pieces resetting between rounds. +- Increased the minimum width of cave tunnels to prevent impassable paths. +- Fixed deconstructor input slots becoming unlocked when starting a new round while the deconstructor is running. +- Fixed Grenade Launcher quality doing basically nothing, because it increased the minuscule amount of blunt force trauma the grenade causes on impact instead of the explosion damage. +- Fixed vitality modifiers not being taken into account in the readings in the health interface. For example, gunshot wounds on the head cause a x2 larger vitality drop than on other limbs, but this wasn't displayed on the health interface. +- Fixed Planet Neon Sign sprite bleed. +- Fixed level resource spawn rate not properly respecting the resource spawn chance values of level generation parameters. +- Fixed some text overflows in the hiring menu when using a small HUD scale. +- Fixed name on an ID card resetting to the original name if you rename a character and then start a new round. +- Fixed handcuffs in the backmost hand being drawn in front of the character. +- Fixed water splashes appearing in an incorrect hull when a character's limb moves from a flooded hull to another hull, where the limb is no longer underwater. +- Fixed crashing when a signal causes a wired item to be dropped (e.g. when you attach a detonator to a destructible ice wall and blow it up). +- Oxygen generators and shelves don't fill up oxygen tanks when on fire. Caused repeated explosions when the tank constantly refilled and re-exploded. +- Fixed "gene harvester" and "deep sea slayer" working on all enemies, not just monsters. +- Fixed floating point inaccuracy sometimes preventing items from being used as fabrication ingredients (e.g. an oxygen generator may sometimes only fill tanks up to something like 99.9998%, which prevented it from being used in recipes that require a full tank). +- Fixed item picking timer (e.g. detaching an item from a wall) ticking down when the game is paused. +- Fixed outpost supply cabinets missing the oxygen tank spawns. +- Made the water current outside the levels start from the same point where monsters start heading towards the level, to make sure monsters can't escape too far from subs with a weak engine. +- Fixed hardened diving knife recipe. +- Fixed probability multiplier not being shown in wearable tooltip if the damage multiplier is 1. +- Yet another attempt to prevent beacon missions from failing for apparently no reason: sonar monitors won't get damaged by water after the beacon's been activated. +- Fixed text selection in a textbox stopping when the cursor goes outside the box. +- Fixed fire, breach and intruder report icons not being shown to anyone. +- Fixed missing/unwired lighting in ResearchModule_02_Colony. +- Remove particles when switching screens (otherwise e.g. particles from the previous round are still in the level if you happen to be looking at the right spot). +- Thalamus or ice walls can't be welded. +- Quick-reloading tries to reload the item whose contained items have the lowest condition. In other words, if you've equipped 2 weapons, quick-reloading reloads the one with the least ammo instead of the one that's the first in your inventory. +- Fixed erroneous dementonite and depleted fuel tool recipes. +- Fixed swapping a scaled turret/hardpoint causing the new one to be misplaced. +- Fixed inability to upgrade the sub or do maintenance if you buy and opt to switch to a new sub, and then go to the submarine switch terminal to cancel the switching. +- Fixed stolen items becoming non-stolen when deconstructed. +- Fixed ItemContainer UI popping up (with no visible inventory slot) when you pick one up, e.g. picking up a detonator from the floor. +- Fixed "[E] Rewire" hover text being shown on attachable items that haven't been attached to a wall (even though they can't be rewired until attached). +- Fixed trying to bind multiple console commands to the same key with the "bindkey" command crashing the game. +- Fixed high-quality revolvers having no difference to normal-quality ones. They should get a 10% damage boost per quality level but didn't, due to incorrectly configured quality stats. +- Fixed multiple monster missions sometimes spawning the monsters close to each other, causing them to attack each other. +- Fixed monsters sometimes using the wrong animation parameters while idling (or moving slowly). +- Fixed nuclear depth decoy using the same sprite as the normal depth decoy. +- Fixed fractal guardian VitalityMultipliers being configured incorrectly (using the "type" attribute but with affliction identifiers instead of types). +- Fixed incorrectly sized thalamus wall colliders, added background sprites to the walls. +- Fixed "tried to overwrite a submarine that's not in a local package" error when loading and trying to save a submarine autosave file. +- Fixed location portraits sometimes not showing up in the mission tab. Happened when we initialized the mission tab before the portrait had been loaded. +- Fixed Coilguns and Chainguns not always playing the firing sound when fired. Happened because their audio clips were so long (albeit mostly silent) that firing them continuously led to a ton of clips playing simultaneously, exhausting the available audio channels. +- Fixed monster missions' sonar marker being placed incorrectly if a monster ends up inside the sub, making it look as if the monster was far outside the level. This often made it look like the monster was moving away from the sub when trying to approach its position as it appeared on the sonar? +- Fixed bandolier (and other items that give bonuses when worn) giving bonuses when the item is held. +- Fixed mod texts being briefly misaligned when scrolling down the list of unpublished mods. +- Fixed light sprite rotation not getting refreshed when placing an attachable item on a wall when lighting has been disabled with console commands. +- Fixed supercapacitors showing 1% as the initial recharge rate because the recharge rate defaulted to 10. +- Fixed some ending options of the "good samaritan" outpost event not ending the event. +- Fixed random (non-mission) events disappearing from outposts when you save and quit. + +Modding: +- The tutorials are now implemented using the scripted event system, and are fully moddable. New tutorials can be implemented in xml or using the event editor, and the system could potentially be used for other types of content too (scripted "scenarios" perhaps?). +- Added DamageMultiplier and LaunchImpulse to Turret. LaunchImpulse is now defined on turrets instead of ammunition (total impulse is the sum of turret + ammunition). +- Added SnapRopeOnNewAttack property to Attacks: allows characters to switch attacks without snapping ropes from previous attacks. +- Added dividebylimbcount to Explosion, which determines whether the damage is spread out among limbs (if set to true). +- UpgradeCategories with no upgrades in them are hidden from the upgrade menu (i.e. if you modify the upgrades so some of the vanilla categories no longer contain any upgrades, those categories won't be shown). +- Added CheckTalentAction, which can be used in events to check whether a target has unlocked a specific talent. +- Changed how submarine upgrades are calculated: now no longer adds previous levels' cost to the price, but rather relies on higher increasehigh values. +- Made NPC personality traits a separate content type instead of defining them in the localization files. +- Fixed OnDeath status effects defined in afflictions not working. Did not affect any vanilla content. +- Fixed crash when controlling a character with more than 10 "Any" inventory slots. Did not affect any vanilla content. +- Fixed custom husk appendages' textures failing to load. +- Added new properties to StatusEffect's SpawnCharacter feature: Stun, AfflictionOnSpawn, AfflictionStrength, TransferControl, RemovePreviousCharacter, TransferBuffs, TransferAfflictions, TransferInventory. +- Fixed bots always choosing their "personality trait" from the first 6 even if more are modded in. +- Fixed affliction names and descriptions being empty if they're not available in the selected language or configured in the affliction .xml file directly. +- Fixed custom husk afflictions not always working properly, because the vanilla husk affliction was sometimes used instead of the custom husk affliction. +- Fixed ExtraLoad working the wrong way around on PowerTransfer components that generate/consume power (the extra load would supply power to the grid). Does not affect the vanilla game, because neither junction boxes or relays generate or consume power. +- Fixed crashing if Afflictions defined in an Attack can't be found. +- Fixed crashing if a Throwable has an OnActive StatusEffect that removes or kills the user. +- Fixed items/structures falling back to the description defined in the .xml even if it's empty, if the description is not defined for the selected language. Now descriptions fall back to English when not defined for the selected language. +- Fixed the "reloadwearables" and "loadwearable" console commands crashing the game when used outside the character editor. +- Fixed character editor crash if you first reload textures and then recreate the ragdoll. +- Fixed inability to localize item names if the name is defined directly in the item config. +- Allowed defining where mineral mission resources are spawned using the "positiontype" attribute. The supported types are "MainPath", "SidePath", "Cave", and "AbyssCave". +- Fixed console errors when trying to check int values with PropertyConditionals. +- Fixed melee weapon's StrikingPowerMultiplier only affecting the afflictions defined in the Attack, not the ones defined in the status effect. + +--------------------------------------------------------------------------------------------------------- +v0.18.15.2 (MacOS only) +--------------------------------------------------------------------------------------------------------- + +- Fixed crashes on MacOS 10.13 and 10.14. This seems to have happened because Microsoft quietly dropped support for these versions in .NET Core in late 2021, and we didn't realize until this hotfix when we deployed with a sufficiently new version of the technology. +- Fixed inability to use the voice chat on some MacOS versions, due to the game not having permissions to access the microphone as a result of the aforementioned .NET Core upgrade. + --------------------------------------------------------------------------------------------------------- v0.18.15.1 --------------------------------------------------------------------------------------------------------- @@ -49,12 +359,10 @@ Fixes: - Fixed non-player-team interactable items getting transferred on sub switch. - Fixed ballast flora root emitting particles when damaged client-side, even if it's already been destroyed. - Fixed recycle recipes for Piercing Ammunition Box and Pulse Tri-Laser Fuel Box. -- Fixed friendly fire and karma always showing up as disabled on dedicated servers in the server list. - Fixed some lights (e.g. vending machines, neon lights, holographics displays) looking different in the sub editor than they do in-game. - Fixed undocked shuttles remaining undocked if you save and start a new game with the same submarine during the same session. Restarting the game fixed the issue though. - Fixed sonar markers going crazy if the start and end locations have the same name + added some more variety to location names to prevent duplicate location names. - Fixed multiediting an ItemComponent modifying all the components of that type in all the selected items (e.g. when editing the 1st light component of a switch, all lights in all switches would be edited). -- Fixed spineling spikes fired by a human with spineling genes not damaging any human characters (enemies in PvP, pirates in pirate missions) when friendly fire is disabled. - Fixed melee weapons not damaging structures from outside. Modding: @@ -79,6 +387,7 @@ Modding: v0.18.11.0 --------------------------------------------------------------------------------------------------------- +Bugfixes: - Disabled project-wide invariant globalization, which was meant to address "couldn't find a valid ICU package installed on the system" errors on some Linux distributions. The fix caused issues with case-insensitive comparisons and converting to upper or lower case in non-latin alphabets. - Fixed tutorial characters spawning without a headset. - Fixed inability to bind keys to LMB by clicking on the input box. diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 50ed25dac..482899fc2 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -11,7 +11,7 @@ autorestart="false" LevelDifficulty="20" AllowedRandomMissionTypes="Random,Salvage,Monster,Cargo,Combat" - AllowedClientNameChars="32-33,38-46,48-57,65-90,91-91,93-93,95-122,192-255,384-591,1024-1279,19968-21327,21329-40959,13312-19903,131072-173791,173824-178207,178208-183983,63744-64255,194560-195103" + AllowedClientNameChars="32-33,38-46,48-57,65-90,91-91,93-93,95-122,192-255,384-591,1024-1159,1162-1279,19968-21327,21329-40959,13312-19903,131072-173791,173824-178207,178208-183983,63744-64255,194560-195103" ServerMessage="" tickrate="20" randomizeseed="True" diff --git a/Barotrauma/BarotraumaTest/CommonnessInfoTests.cs b/Barotrauma/BarotraumaTest/CommonnessInfoTests.cs new file mode 100644 index 000000000..ee85a5206 --- /dev/null +++ b/Barotrauma/BarotraumaTest/CommonnessInfoTests.cs @@ -0,0 +1,84 @@ +using Barotrauma; +using FluentAssertions; +using FsCheck; +using Xunit; +using static Barotrauma.ItemPrefab; + +namespace TestProject +{ + public class CommonnessInfoTests + { + private class CustomGenerators + { + + public static Arbitrary CommonnessInfoGeneratorOverride() + { + return Arb.From(from float commonness in Arb.Generate().Where(IsValid) + from float? abyssCommonness in Arb.Generate().Where(IsNullableValid) + from float? caveCommonness in Arb.Generate().Where(IsNullableValid) + select new CommonnessInfo(commonness, abyssCommonness, caveCommonness)); + + static bool IsValid(float commonness) => !float.IsNaN(commonness) && commonness > float.MinValue && commonness < float.MaxValue; + static bool IsNullableValid(float? commonness) => !commonness.HasValue || IsValid(commonness.Value); + } + } + + public CommonnessInfoTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void TestInheritedCommonness() + { + Prop.ForAll((child, parent) => + { + var info = child.WithInheritedCommonness(parent); + + info.Commonness.Should().Be(child.commonness); + + if (child.abyssCommonness.HasValue) + { + info.abyssCommonness.Should().HaveValue(); + info.abyssCommonness.Should().Be(child.abyssCommonness); + } + else if (parent.abyssCommonness.HasValue) + { + info.abyssCommonness.Should().HaveValue(); + info.abyssCommonness.Should().Be(parent.abyssCommonness); + } + else + { + info.abyssCommonness.Should().NotHaveValue(); + } + + if (child.caveCommonness.HasValue) + { + info.caveCommonness.Should().HaveValue(); + info.caveCommonness.Should().Be(child.caveCommonness); + } + else if (parent.caveCommonness.HasValue) + { + info.caveCommonness.Should().HaveValue(); + info.caveCommonness.Should().Be(parent.caveCommonness); + } + else + { + info.caveCommonness.Should().NotHaveValue(); + } + }).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestPathCommonness() + { + Prop.ForAll(info => + { + info.GetCommonness(Level.TunnelType.MainPath).Should().Be(info.Commonness); + info.GetCommonness(Level.TunnelType.SidePath).Should().Be(info.Commonness); + info.GetCommonness(Level.TunnelType.Cave).Should().Be(info.CaveCommonness); + }).QuickCheckThrowOnFailure(); + } + } +} diff --git a/Barotrauma/BarotraumaTest/EndpointComparisonTests.cs b/Barotrauma/BarotraumaTest/EndpointComparisonTests.cs new file mode 100644 index 000000000..6e588b4f3 --- /dev/null +++ b/Barotrauma/BarotraumaTest/EndpointComparisonTests.cs @@ -0,0 +1,15 @@ +#nullable enable +using System.Net; +using Barotrauma.Networking; +using Xunit; + +namespace TestProject; + +public class EndpointComparisonTests +{ + [Fact] + public void TestLidgrenAddress() + { + Assert.True(new LidgrenAddress(IPAddress.Loopback) == new LidgrenAddress(IPAddress.IPv6Loopback)); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/EndpointParseTests.cs b/Barotrauma/BarotraumaTest/EndpointParseTests.cs new file mode 100644 index 000000000..c8ccb4e43 --- /dev/null +++ b/Barotrauma/BarotraumaTest/EndpointParseTests.cs @@ -0,0 +1,67 @@ +#nullable enable +using System.Net; +using Barotrauma; +using Xunit; +using Barotrauma.Networking; +using FluentAssertions; + +namespace TestProject; + +public class EndpointParseTests +{ + [Fact] + public void TestLidgrenEndpoint() + { + Endpoint.Parse("127.0.0.1:27015") + .Should() + .BeOfType>() + .And.BeEquivalentTo( + Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), + options => options.RespectingRuntimeTypes()); + } + + [Fact] + public void TestLidgrenEndpointHostName() + { + Endpoint.Parse("localhost:27015") + .Should() + .BeOfType>() + .And.BeEquivalentTo( + Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), + options => options.RespectingRuntimeTypes()); + } + + [Fact] + public void TestLidgrenAddress() + { + Address.Parse("127.0.0.1") + .Should() + .BeOfType>() + .And.BeEquivalentTo( + Option
.Some(new LidgrenAddress(IPAddress.Loopback)), + options => options.RespectingRuntimeTypes()); + } + + [Fact] + public void TestSteamP2PEndpoint() + { + Endpoint.Parse("STEAM_1:1:508792388") + .Should() + .BeOfType>() + .And.BeEquivalentTo( + Option.Some(new SteamP2PEndpoint(new SteamId(76561198977850505))), + options => options.RespectingRuntimeTypes()); + } + + [Fact] + public void TestSteamP2PAddress() + { + Address.Parse("STEAM_1:1:508792388") + .Should() + .BeOfType>() + .And.BeEquivalentTo( + Option
.Some(new SteamP2PAddress(new SteamId(76561198977850505))), + options => options.RespectingRuntimeTypes()); + new SteamId(76561198977850505).StringRepresentation.Should().BeEquivalentTo("STEAM_1:1:508792388"); + } +} diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs new file mode 100644 index 000000000..1e499dac0 --- /dev/null +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Reflection; +using Barotrauma; +using Xunit; + +namespace TestProject; + +public class INetSerializableStructImplementationChecks +{ + private delegate bool TryFindBehaviorDelegate(Type type, out NetSerializableProperties.IReadWriteBehavior behavior); + + [Fact] + public void CheckStructMemberTypes() + { + var interfaceType = typeof(INetSerializableStruct); + var types = interfaceType.Assembly.GetTypes() + .Where(t => !t.IsAbstract && !t.IsInterface && t.IsAssignableTo(interfaceType)); + + //private static bool TryFindBehavior(Type type, out IReadWriteBehavior behavior) + TryFindBehaviorDelegate tryFindBehavior + = typeof(NetSerializableProperties) + .GetMethod("TryFindBehavior", BindingFlags.NonPublic | BindingFlags.Static, + typeof(TryFindBehaviorDelegate).GetMethod("Invoke")! + .GetParameters().Select(p => p.ParameterType).ToArray())! + .CreateDelegate(); + + foreach (var type in types) + { + var members = NetSerializableProperties.GetPropertiesAndFields(type); + foreach (var member in members) + { + void checkType(Type typeBeingChecked) + { + Assert.True(tryFindBehavior(typeBeingChecked, out _), $"{type}.{member.Name} of type {member.Type} is unsupported in {nameof(INetSerializableStruct)}"); + Type? nestedType = null; + if (typeBeingChecked.IsGenericType) + { + nestedType = typeBeingChecked.GetGenericArguments()[0]; + } + else if (typeBeingChecked.IsArray) + { + nestedType = typeBeingChecked.GetElementType(); + } + + if (nestedType != null) + { + checkType(nestedType); + } + } + checkType(member.Type); + } + } + } +} diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs index 83cedcc37..e319baede 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Immutable; using Barotrauma; using Barotrauma.Networking; using FluentAssertions; @@ -11,7 +12,7 @@ using Xunit; namespace TestProject { // ReSharper disable UnusedMember.Local NotAccessedField.Local UnusedMember.Global - public class INetSerializableStructTests + public sealed class INetSerializableStructTests { private class CustomGenerators { @@ -25,6 +26,21 @@ namespace TestProject Arb.Register(); } + [Fact] + public void TestBitField() + { + Prop.ForAll(SerializeDeserializeBitField).VerboseCheckThrowOnFailure(); + } + + [Fact] + public void TestRanged() + { + Prop.ForAll( + Arb.Generate().Where(i => i <= 100 && i >= -100).ToArbitrary(), + Arb.Generate().Where(f => f <= 100f && f >= -100f).ToArbitrary(), + SerializeDeserializeRanged).QuickCheckThrowOnFailure(); + } + [Fact] public void TestOptional() { @@ -58,6 +74,12 @@ namespace TestProject { Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); } + + [Fact] + public void TestEnumFlags() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } [Fact] public void TestArray() @@ -67,6 +89,14 @@ namespace TestProject Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); } + [Fact] + public void TestImmutableArray() + { + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + [Fact] public void TestNullable() { @@ -147,6 +177,26 @@ namespace TestProject Thousand = 1000 } + [Flags] + private enum EnumFlagsTest + { + None = 0, + Bit0 = 1 << 0, + Bit1 = 1 << 1, + Bit2 = 1 << 2, + Bit3 = 1 << 3 + } + + private struct TestRangedStruct : INetSerializableStruct + { + [NetworkSerialize(MinValueInt = -100, MaxValueInt = 100)] + public int IntValue; + + [NetworkSerialize(MinValueFloat = -100, MaxValueFloat = 100, NumberOfBits = 16)] + public float FloatValue; + } + +#pragma warning disable CS0649 private struct TestStruct : INetSerializableStruct { [NetworkSerialize] @@ -168,6 +218,25 @@ namespace TestProject public (T, U) NotSerializedValue; public (T, U) NotSerializedFunction() => throw new NotImplementedException(); } +#pragma warning restore CS0649 + + private static void SerializeDeserializeRanged(int intValue, float floatValue) + { + ReadWriteMessage msg = new ReadWriteMessage(); + TestRangedStruct writeStruct = new TestRangedStruct + { + IntValue = intValue, + FloatValue = floatValue + }; + + msg.WriteNetSerializableStruct(writeStruct); + msg.BitPosition = 0; + + TestRangedStruct readStruct = INetSerializableStruct.Read(msg); + + readStruct.FloatValue.Should().BeApproximately(floatValue, 0.25f); // should be enough precision + readStruct.IntValue.Should().Be(intValue); + } private static void SerializeDeserialize(T arg) where T : notnull { @@ -177,12 +246,14 @@ namespace TestProject Value = arg }; - ((INetSerializableStruct)writeStruct).Write(msg); + msg.WriteNetSerializableStruct(writeStruct); msg.BitPosition = 0; TestStruct readStruct = INetSerializableStruct.Read>(msg); - readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + readStruct.Should().BeEquivalentTo(writeStruct, options => options + .ComparingByMembers>() + .ComparingByMembers(typeof(Option<>))); } private static void SerializeDeserializeNullableTuple(T arg1, U arg2) @@ -194,12 +265,35 @@ namespace TestProject Two = arg2 }; - ((INetSerializableStruct)writeStruct).Write(msg); + msg.WriteNetSerializableStruct(writeStruct); msg.BitPosition = 0; TupleNullableStruct readStruct = INetSerializableStruct.Read>(msg); - readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + readStruct.Should().BeEquivalentTo(writeStruct, options => options + .ComparingByMembers>() + .ComparingByMembers(typeof(Option<>))); + } + + private static void SerializeDeserializeBitField(bool[] arg) + { + ReadWriteMessage msg = new ReadWriteMessage(); + IWritableBitField bitFieldWrite = new WriteOnlyBitField(); + + foreach (bool b in arg) + { + bitFieldWrite.WriteBoolean(b); + } + + bitFieldWrite.WriteToMessage(msg); + msg.BitPosition = 0; + + IReadableBitField bitFieldRead = new ReadOnlyBitField(msg); + + foreach (bool b in arg) + { + bitFieldRead.ReadBoolean().Should().Be(b); + } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/NetIdUtilsTests.cs b/Barotrauma/BarotraumaTest/NetIdUtilsTests.cs new file mode 100644 index 000000000..90bd2dc50 --- /dev/null +++ b/Barotrauma/BarotraumaTest/NetIdUtilsTests.cs @@ -0,0 +1,20 @@ +using System; +using Barotrauma.Networking; +using FluentAssertions; +using FsCheck; +using Xunit; + +namespace TestProject; + +public class NetIdUtilsTests +{ + [Fact] + public void TestGetIdOlderThan() + { + Prop.ForAll(id => + { + var olderId = NetIdUtils.GetIdOlderThan(id); + Assert.True(NetIdUtils.IdMoreRecent(id, olderId)); + }).QuickCheckThrowOnFailure(); + } +} \ No newline at end of file diff --git a/BuildScripts/app_1026340.vdf b/BuildScripts/app_1026340.vdf new file mode 100644 index 000000000..70d3550a6 --- /dev/null +++ b/BuildScripts/app_1026340.vdf @@ -0,0 +1,15 @@ +"appbuild" +{ + "appid" "1026340" + "desc" "" + "buildoutput" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\output" + "contentroot" "" + "setlive" "" + "preview" "0" + "local" "" + "depots" + { + "1026341" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_1026341.vdf" + "1026342" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_1026342.vdf" + } +} diff --git a/BuildScripts/app_602960.vdf b/BuildScripts/app_602960.vdf new file mode 100644 index 000000000..f2fca8b7f --- /dev/null +++ b/BuildScripts/app_602960.vdf @@ -0,0 +1,16 @@ +"appbuild" +{ + "appid" "602960" + "desc" "" + "buildoutput" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\output" + "contentroot" "" + "setlive" "" + "preview" "0" + "local" "" + "depots" + { + "602961" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602961.vdf" + "602962" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602962.vdf" + "602963" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602963.vdf" + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 770fb43d8..05b530ad5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,12 +16,12 @@ Before you start doing modifications to the code or submitting pull requests to ### Getting started #### Windows and macOS -You need a version of Visual Studio that supports C# 8.0 to compile game. If you don't have a compatible version of Visual Studio installed, you can get the latest version of Visual Studio from the following link: https://visualstudio.microsoft.com/ +You need a version of Visual Studio that supports C# 10 to compile game. If you don't have a compatible version of Visual Studio installed, you can get the latest version of Visual Studio from the following link: https://visualstudio.microsoft.com/ When installing on Windows, make sure you select ".NET desktop development" during the install process to make sure you have the required features to work with Barotrauma. #### Linux -You will need to install the .NET Core 3.0 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904 +You will need to install the .NET 6 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux To edit the source code, we recommend using [Visual Studio Code](https://code.visualstudio.com/) with [Microsoft's C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp). @@ -38,7 +38,7 @@ To develop from Visual Studio, open the solution that corresponds to the platfor You can also use your favorite source editor and build through the command line by navigating to the projects you wish to build and running the following command: `dotnet build [project].csproj -c [Debug/Release] /p:Platform=x64` -To deploy for release, run the scripts in the `Deploy` directory; the resulting binaries you'll want to redistribute should be found at `Barotrauma/bin/Release[Windows/Mac/Linux]/netcoreapp3.0/[win-x64/osx-x64/linux-x64]/publish` +To build for release, run one of the scripts found in the `Deploy` directory. The resulting binaries should be found at `Barotrauma/Deploy/bin/content`. The `BarotraumaShared/Content` folder, which contains Barotrauma's art, item XMLs, sounds, and other assets, is not included in the GitHub repository. If you have a legal copy of the game, you can copy the `Content` folder from the game's files to `BarotraumaShared/Content`. diff --git a/Deploy/DeployAll.bat b/Deploy/DeployAll.bat new file mode 100644 index 000000000..b7f7b1ae7 --- /dev/null +++ b/Deploy/DeployAll.bat @@ -0,0 +1,4 @@ +@ECHO OFF + +cd DeployAll +dotnet run -v q diff --git a/Deploy/DeployAll.sh b/Deploy/DeployAll.sh new file mode 100644 index 000000000..511cf2276 --- /dev/null +++ b/Deploy/DeployAll.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd DeployAll +dotnet run -v q diff --git a/Deploy/DeployAll/DeployAll.csproj b/Deploy/DeployAll/DeployAll.csproj new file mode 100644 index 000000000..72669ffd0 --- /dev/null +++ b/Deploy/DeployAll/DeployAll.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + disable + enable + + + + true + + + + true + + + + + + + diff --git a/Deploy/DeployAll/Deployables.cs b/Deploy/DeployAll/Deployables.cs new file mode 100644 index 000000000..a83936516 --- /dev/null +++ b/Deploy/DeployAll/Deployables.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace DeployAll; + +public static class Deployables +{ + public const string ResultPath = "Deploy/bin/content"; + + private const string clientProjFmt = "Barotrauma/BarotraumaClient/{0}Client.csproj"; + private const string serverProjFmt = "Barotrauma/BarotraumaServer/{0}Server.csproj"; + + private static readonly ImmutableArray<(string Project, string Runtime)> platforms = new[] + { + ("Windows", "win-x64"), + ("Mac", "osx-x64"), + ("Linux", "linux-x64") + }.ToImmutableArray(); + + public static void Generate(string configuration, Version version, string gitBranch, string gitRevision) + { + Util.RecreateDirectory(ResultPath); + + File.WriteAllText( + Path.Combine(ResultPath, "readme.txt"), + $"This is Barotrauma {configuration} v{version} ({gitBranch}, {gitRevision}) built on {DateTime.Now}"); + + foreach (var (project, runtime) in platforms) + { + string serverPath = Path.Combine(ResultPath, project, "Server"); + + void checkVersion(string projPath) + { + Version projVersion = Version.Parse( + XDocument.Load(projPath).Root? + .Element("PropertyGroup")? + .Element("Version")? + .Value ?? throw new Exception($"Version not found in {projPath}")); + if (projVersion != version) + { + throw new Exception($"Version mismatch in {projPath}: {projVersion} != {version}"); + } + } + + string serverProj = string.Format(serverProjFmt, project); + string clientProj = string.Format(clientProjFmt, project); + + checkVersion(serverProj); + checkVersion(clientProj); + + Console.WriteLine( + $"*** Building Barotrauma {configuration}{project} v{version} ({gitBranch}, {gitRevision}) to \"{Path.Combine(ResultPath, project)}\" ***"); + + DotnetCmd.Publish( + projPath: serverProj, + configuration: configuration, + runtime: runtime, + resultPath: serverPath); + Util.DeleteFiles(serverPath, + "*.png", "*.ogg", "*.webm", + "*.mp4", "*.otf", "*.ttf"); + + string clientPath = Path.Combine(ResultPath, project, "Client"); + string clientBundlePath = clientPath; + + if (project == "Mac") + { + clientPath = Path.Combine(clientPath, "Barotrauma.app", "Contents", "MacOS"); + Util.CopyDirectory("Deploy/DeployAll/macSkeleton", clientBundlePath); + + string infoPlistPath = Path.Combine(clientBundlePath, "Barotrauma.app", "Contents", "info.plist"); + string infoPlist = File.ReadAllText(infoPlistPath, Encoding.UTF8) + .Replace("{short_version_string}", $"{version.Major}.{version.Minor}.{version.Build}") + .Replace("{version}", version.ToString()) + .Replace("{current_year}", DateTime.Now.Year.ToString()); + File.WriteAllText(infoPlistPath, infoPlist, Encoding.UTF8); + } + + DotnetCmd.Publish( + projPath: serverProj, + configuration: configuration, + runtime: runtime, + resultPath: clientPath); + DotnetCmd.Publish( + projPath: clientProj, + configuration: configuration, + runtime: runtime, + resultPath: clientPath); + + if (!File.Exists(Path.Combine(clientPath, "GameAnalytics.NetStandard.dll"))) + { + throw new Exception($"GameAnalytics was not found in \"{clientPath}\""); + } + + if (project == "Mac") + { + Util.CopyDirectory(Path.Combine(clientPath, "Content", "Effects"), + Path.Combine( + clientBundlePath, "Barotrauma.app", "Contents", "Resources", "Content", "Effects")); + Util.CopyDirectory(Path.Combine(clientPath, "Content", "Lights"), + Path.Combine( + clientBundlePath, "Barotrauma.app", "Contents", "Resources", "Content", "Lights")); + } + + Console.WriteLine(""); + } + } +} \ No newline at end of file diff --git a/Deploy/DeployAll/DotnetCmd.cs b/Deploy/DeployAll/DotnetCmd.cs new file mode 100644 index 000000000..c6435e313 --- /dev/null +++ b/Deploy/DeployAll/DotnetCmd.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Xml.Linq; +using AsmResolver.PE; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources.Builder; + +namespace DeployAll; + +public static class DotnetCmd +{ + private const string DotnetAppName = "dotnet"; + + private const string desiredRuntimeVersion = "6.0.8"; + + public static void Publish(string projPath, string configuration, string runtime, string resultPath) + { + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = DotnetAppName, + ArgumentList = + { + "publish", + projPath, + "-c", + configuration, + "-clp:ErrorsOnly;Summary", + "--self-contained", + "-r", + runtime, + "/p:Platform=x64", + "/p:ErrorOnDuplicatePublishOutputFiles=false", //TODO: fix our duplicate files + "/p:RollForward=Disable", + $"/p:RuntimeFrameworkVersion={desiredRuntimeVersion}", + "-o", + resultPath + }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + var process = Util.StartProcess(psi); + process.WaitForExit(); + string stdout = process.StandardOutput.ReadToEnd(); + string stderr = process.StandardError.ReadToEnd(); + + string errorLine = $"{stdout}\n{stderr}".Split('\n') + .First(ln => ln.Contains("Error(s)", StringComparison.OrdinalIgnoreCase)) + .Trim(); + + if (!errorLine.StartsWith("0 ", StringComparison.OrdinalIgnoreCase)) + { + throw new Exception($"Failed to build {projPath}, {errorLine}"); + } + + Console.WriteLine($" - Published \"{projPath}\" to \"{resultPath}\", {errorLine}"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !runtime.StartsWith("win")) { return; } + + // You may be wondering, what is this crap? + // Cross-compiling is something that should work perfectly, because it's super convenient. + // However, thanks to the way .NET works, cross-compiling to Windows from *nix platforms + // results in an executable with basically no metadata, and the wrong subsystem! + // (see https://github.com/dotnet/sdk/blob/375955d3a9de213a01d70eb6180298000dee30ee/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs#L127-L132) + // Does it look like we're about to modify the SDK itself to solve this problem? Yeah right. + // Instead let's just take the shim generated by the SDK and fix it ourselves. + + XElement firstPropertyGroup = XDocument.Load(projPath) + .Root? + .Element("PropertyGroup") + ?? throw new Exception("PropertyGroup not found"); + + string assemblyName = firstPropertyGroup.Element("AssemblyName")?.Value + ?? throw new Exception("AssemblyName not found"); + + // This is the shim that doesn't have the stuff we want. + var fileToChange = PEFile.FromFile(Path.Combine(resultPath, $"{assemblyName}.exe")); + // Luckily, the SDK does embed all of that data in the assembly with all of the IL! + // We can just yoink it from here. + var managedAssembly = PEImage.FromFile(Path.Combine(resultPath, $"{assemblyName}.dll")); + + // Here's a whole lot of magic to set up the resources section of the executable + var resourceSection = new PESection(".rsrc", SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + var resourceDirectoryBuffer = new ResourceDirectoryBuffer(); + resourceDirectoryBuffer.AddDirectory(managedAssembly.Resources ?? throw new Exception($"{assemblyName}.dll has no resources")); + resourceSection.Contents = resourceDirectoryBuffer; + fileToChange.Sections.Add(resourceSection); + fileToChange.AlignSections(); + var dataDirectories = fileToChange.OptionalHeader.DataDirectories; + dataDirectories[2] = new DataDirectory(resourceDirectoryBuffer.Rva, resourceDirectoryBuffer.GetPhysicalSize()); + + // And here's something a little less magical that fixes the subsystem + fileToChange.OptionalHeader.SubSystem = firstPropertyGroup.Element("OutputType")?.Value == "WinExe" + ? SubSystem.WindowsGui + : SubSystem.WindowsCui; + + using var writeStream = File.Open(Path.Combine(resultPath, $"{assemblyName}.exe"), FileMode.Create); + fileToChange.Write(writeStream); + } + + public static Version GetSdkVersion() + { + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = DotnetAppName, + ArgumentList = + { + "--version" + }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + var process = Util.StartProcess(psi); + process.WaitForExit(); + string stdout = process.StandardOutput.ReadToEnd(); + + return Version.Parse(stdout.Trim()); + } +} diff --git a/Deploy/DeployAll/GitCmd.cs b/Deploy/DeployAll/GitCmd.cs new file mode 100644 index 000000000..624c418e3 --- /dev/null +++ b/Deploy/DeployAll/GitCmd.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; + +namespace DeployAll; + +public static class GitCmd +{ + private const string gitCmdName = "git"; + + private static ProcessStartInfo MakePsi(params string[] args) + { + var psi = new ProcessStartInfo + { + FileName = gitCmdName, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + foreach (var arg in args) + { + psi.ArgumentList.Add(arg); + } + + return psi; + } + + private static void ExecCmd(out string stdOut, out string stdErr, params string[] args) + { + var process = Util.StartProcess(MakePsi(args)); + process.WaitForExit(); + stdOut = process.StandardOutput.ReadToEnd(); + stdErr = process.StandardError.ReadToEnd(); + } + + public static string GetRevision() + { + ExecCmd(out string stdOut, out _, + "rev-parse", + "--short", + "HEAD"); + + return stdOut.Trim(); + } + + public static string GetBranch() + { + ExecCmd(out string stdOut, out _, + "branch", + "--show-current"); + + return stdOut.Trim(); + } + + public static bool HasUncommittedChanges() + { + ExecCmd(out string stdOut, out _, + "status", + "--porcelain=1"); + + return !string.IsNullOrWhiteSpace(stdOut); + } + + public static bool IsRepoOutOfSync() + { + ExecCmd(out _, out _, + "fetch"); + + ExecCmd(out string remoteBranch, out _, + "status", + "-sb"); + + if (!remoteBranch.StartsWith("##")) { return true; } + if (!remoteBranch.Contains("...")) { return true; } + + remoteBranch = remoteBranch[(remoteBranch.IndexOf("...", StringComparison.InvariantCulture) + 3)..]; + remoteBranch = remoteBranch[..remoteBranch.IndexOf("\n", StringComparison.InvariantCulture)]; + + string localRevision = GetRevision(); + ExecCmd(out string remoteRevision, out _, + "rev-parse", + "--short", + remoteBranch); + remoteRevision = remoteRevision.Trim(); + + return localRevision != remoteRevision; + } +} \ No newline at end of file diff --git a/Deploy/DeployAll/Program.cs b/Deploy/DeployAll/Program.cs new file mode 100644 index 000000000..3de060435 --- /dev/null +++ b/Deploy/DeployAll/Program.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using DeployAll; + +while (!Directory.GetFiles(".").Any(f => f.EndsWith(".sln"))) +{ + Directory.SetCurrentDirectory(".."); +} + +const string windowsClientProj = "Barotrauma/BarotraumaClient/WindowsClient.csproj"; + +Version gameVersion = Version.Parse( + XDocument.Load(windowsClientProj).Root? + .Element("PropertyGroup")? + .Element("Version")? + .Value ?? throw new Exception($"Version not found in {windowsClientProj}")); + +string gitRevision = GitCmd.GetRevision(); +string gitBranch = GitCmd.GetBranch(); + +Console.WriteLine($"DEPLOYALL - Barotrauma v{gameVersion}, branch {gitBranch}, revision {gitRevision}"); + +if (GitCmd.HasUncommittedChanges()) +{ + if (Util.AskQuestion("The repo currently has some uncommitted changes. Do you still wish to proceed? [y/n]") + .AnsweredNo()) { return; } +} +else if (GitCmd.IsRepoOutOfSync()) +{ + if (Util.AskQuestion("The repo is currently out of sync. Do you still wish to proceed? [y/n]") + .AnsweredNo()) { return; } +} + +var sdkVersion = DotnetCmd.GetSdkVersion(); +Console.WriteLine($"Using .NET SDK {sdkVersion}"); + +string configuration = Util.AskQuestion("Type 1 for Release, 2 for Unstable, enter nothing to cancel") switch +{ + "1" => "Release", + "2" => "Unstable", + _ => "" +}; +if (string.IsNullOrWhiteSpace(configuration)) { return; } + +Deployables.Generate(configuration, gameVersion, gitBranch, gitRevision); + +if (Util.AskQuestion("Would you like to upload the generated builds to Steam? [y/n]") + .AnsweredNo()) { return; } + +SteamPipeAssistant.PrepareSteamCmd(); +SteamPipeAssistant.PrepareScripts(configuration, gameVersion, gitBranch, gitRevision); + +string userName = Util.AskQuestion("Type your Steam username to upload to Steamworks, enter nothing to skip uploading"); +if (string.IsNullOrWhiteSpace(userName)) { return; } + +SteamPipeAssistant.Upload(userName, configuration); diff --git a/Deploy/DeployAll/SteamPipeAssistant.cs b/Deploy/DeployAll/SteamPipeAssistant.cs new file mode 100644 index 000000000..e111af644 --- /dev/null +++ b/Deploy/DeployAll/SteamPipeAssistant.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; + +namespace DeployAll; + +public static class SteamPipeAssistant +{ + private abstract record ScriptItem(string Name) + { + public abstract override string ToString(); + } + + private record SingleItem(string Name, string Value) : ScriptItem(Name) + { + public override string ToString() => $"\"{Name}\" \"{Value}\""; + } + + private record AggregateItem(string Name, params ScriptItem[] SubItems) : ScriptItem(Name) + { + public override string ToString() + { + return $"\"{Name}\"\n" + + "{\n" + + string.Join("\n", + SubItems.Select(it => it.ToString()) + .SelectMany(s => s.Split("\n")) + .Select(s => $"\t{s}")) + + "\n}"; + } + } + + private static string steamCmdUrl + => true switch + { + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + => "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + => "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz", + _ when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + => "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_osx.tar.gz", + _ => throw new Exception($"Unsupported host platform: {RuntimeInformation.OSDescription}") + }; + + private static string[] steamCmdFilenames + => true switch + { + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + => new[] { "steamcmd.exe" }, + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + => new[] { "steamcmd.sh", "linux32/steamcmd", "linux32/steamerrorreporter" }, + _ when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + => new[] { "steamcmd.sh", "steamcmd" }, + _ => throw new Exception($"Unsupported host platform: {RuntimeInformation.OSDescription}") + }; + + private const string SteamCmdPath = "Deploy/bin/steamcmd"; + + public static void PrepareSteamCmd() + { + if (Directory.Exists(SteamCmdPath)) + { + Console.WriteLine($"SteamCMD found at {SteamCmdPath}, skipping download"); + return; + } + Console.WriteLine($"Downloading SteamCMD to {SteamCmdPath}"); + + Util.RecreateDirectory(SteamCmdPath); + + var steamCmdPkg = Util.DownloadFile(steamCmdUrl).ToArray(); + + if (Path.GetExtension(steamCmdUrl) == ".zip") + { + using var memStream = new MemoryStream(steamCmdPkg); + using ZipArchive archive = new ZipArchive(memStream, ZipArchiveMode.Read); + archive.ExtractToDirectory(SteamCmdPath); + } + else + { + string downloadResultPath = Path.Combine(SteamCmdPath, Path.GetFileName(steamCmdUrl)); + File.WriteAllBytes(downloadResultPath, steamCmdPkg); + + var psi = new ProcessStartInfo + { + FileName = "tar", + ArgumentList = + { + "-xf", + downloadResultPath, + "-C", + SteamCmdPath + }, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + var process = Util.StartProcess(psi); + process.WaitForExit(); + + File.Delete(downloadResultPath); + + foreach (var filename in steamCmdFilenames) + { + psi = new ProcessStartInfo + { + FileName = "chmod", + ArgumentList = + { + "+x", + Path.Combine(SteamCmdPath, filename) + }, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + process = Util.StartProcess(psi); + process.WaitForExit(); + } + } + + Console.WriteLine("SteamCMD downloaded and extracted"); + } + + private const string ScriptPath = "Deploy/bin/scripts"; + private const string BuildOutput = "Deploy/bin/output"; + + private const string appIdScriptFileFmt = "app_{0}.vdf"; + + private const ulong ClientAppId = 602960; + private const ulong ClientWindowsDepotId = 602961; + private const ulong ClientLinuxDepotId = 602962; + private const ulong ClientMacDepotId = 602963; + + private const ulong ServerAppId = 1026340; + private const ulong ServerWindowsDepotId = 1026341; + private const ulong ServerLinuxDepotId = 1026342; + + private static ScriptItem PrepareDepotScript(ulong depotId, string contentPath) + { + var childItems = new List + { + new SingleItem("DepotID", depotId.ToString()), + new SingleItem("contentroot", contentPath), + new AggregateItem("FileMapping", + new SingleItem("LocalPath", "*"), + new SingleItem("DepotPath", "."), + new SingleItem("recursive", "1")), + new SingleItem("FileExclusion", "config_player.xml"), + new SingleItem("FileExclusion", "Thumbs.db"), + new SingleItem("FileExclusion", ".DS_Store"), + new SingleItem("FileExclusion", "__MACOSX"), + }; + + if (depotId == ClientMacDepotId) + { + childItems.Add(new SingleItem("InstallScript", "Barotrauma.app/installscript.vdf")); + } + + var script = new AggregateItem("DepotBuildConfig", childItems.ToArray()); + var scriptFileName = Path.Combine(ScriptPath, $"depot_{depotId}.vdf"); + File.WriteAllText(scriptFileName, script.ToString()); + return new SingleItem(depotId.ToString(), Path.GetFullPath(scriptFileName)); + } + + private static void PrepareAppScript(ulong appId, string configuration, Version version, string gitBranch, string gitRevision) + { + var depotScripts = new AggregateItem("depots", appId switch + { + ClientAppId => new[] + { + PrepareDepotScript(ClientWindowsDepotId, + Path.Combine("Windows", "Client")), + PrepareDepotScript(ClientMacDepotId, + Path.Combine("Mac", "Client")), + PrepareDepotScript(ClientLinuxDepotId, + Path.Combine("Linux", "Client")) + }, + ServerAppId => new[] + { + PrepareDepotScript(ServerWindowsDepotId, + Path.Combine("Windows", "Server")), + PrepareDepotScript(ServerLinuxDepotId, + Path.Combine("Linux", "Server")) + }, + _ => throw new InvalidOperationException() + }); + + var script = new AggregateItem("appbuild", + new SingleItem("appid", appId.ToString()), + new SingleItem("desc", $"{configuration} v{version} ({gitBranch}, {gitRevision})"), + new SingleItem("buildoutput", Path.GetFullPath(BuildOutput)), + new SingleItem("contentroot", Path.GetFullPath(Deployables.ResultPath)), + new SingleItem("setlive", appId switch + { + ClientAppId => "experimental", + ServerAppId => "development", + _ => throw new InvalidOperationException() + }), + new SingleItem("preview", "0"), + depotScripts); + + var scriptFileName = Path.Combine(ScriptPath, string.Format(appIdScriptFileFmt, appId)); + File.WriteAllText(scriptFileName, script.ToString()); + } + + public static void PrepareScripts(string configuration, Version version, string gitBranch, string gitRevision) + { + Console.WriteLine($"Preparing SteamPipe scripts for {configuration} v{version} ({gitBranch}, {gitRevision})"); + + Util.RecreateDirectory(ScriptPath); + + PrepareAppScript(ClientAppId, configuration, version, gitBranch, gitRevision); + PrepareAppScript(ServerAppId, configuration, version, gitBranch, gitRevision); + + Console.WriteLine(""); + } + + public static void Upload(string userName, string configuration) + { + Util.RecreateDirectory(BuildOutput); + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = Path.Combine(SteamCmdPath, steamCmdFilenames.First()), + ArgumentList = + { + "+login", + userName + }, + RedirectStandardOutput = false, + RedirectStandardError = false + }; + + void addScriptCmd(ulong appId) + { + psi.ArgumentList.Add("+run_app_build"); + psi.ArgumentList.Add(Path.GetFullPath(Path.Combine(ScriptPath, string.Format(appIdScriptFileFmt, appId)))); + } + addScriptCmd(ClientAppId); + if (configuration == "Release") { addScriptCmd(ServerAppId); } + + psi.ArgumentList.Add("+quit"); + var process = Util.StartProcess(psi); + process.WaitForExit(); + } +} \ No newline at end of file diff --git a/Deploy/DeployAll/Util.cs b/Deploy/DeployAll/Util.cs new file mode 100644 index 000000000..6962ac512 --- /dev/null +++ b/Deploy/DeployAll/Util.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; + +namespace DeployAll; + +public static class Util +{ + public static void DeleteFiles(string path, params string[] patterns) + { + foreach (var file in patterns.SelectMany(p => Directory.GetFiles(path, p, SearchOption.AllDirectories))) + { + File.Delete(file); + string dir = file; + do + { + dir = Path.GetDirectoryName(dir) ?? ""; + if (Directory.GetFiles(dir, "*", SearchOption.AllDirectories).Length == 0) + { + Directory.Delete(dir, recursive: false); + } + else + { + break; + } + } while (dir.LastIndexOf('/') > 0); + } + } + + public static void CopyDirectory(string sourceDir, string destinationDir) + { + var dir = new DirectoryInfo(sourceDir); + + DirectoryInfo[] dirs = dir.GetDirectories(); + + Directory.CreateDirectory(destinationDir); + + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir); + } + } + + public static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + + public static void RecreateDirectory(string path) + { + DeleteDirectory(path); + Directory.CreateDirectory(path); + } + + public static IReadOnlyList DownloadFile(string url) + { + var httpClient = new HttpClient(); + var response = httpClient.Send(new HttpRequestMessage( + HttpMethod.Get, + new Uri(url))); + using var stream = response.Content.ReadAsStream(); + + using var reader = new BinaryReader(stream); + var contents = new List(); + while (true) + { + byte[] bytesRead = reader.ReadBytes(1024); + if (bytesRead.Length == 0) { break; } + contents.AddRange(bytesRead); + } + + return contents; + } + + public static string AskQuestion(string question) + { + Console.WriteLine(question); + Console.Write("> "); + string answer = Console.ReadLine() ?? ""; + Console.WriteLine(""); + return answer; + } + + public static bool AnsweredYes(this string answer) + => answer.Equals("y", StringComparison.InvariantCulture); + + public static bool AnsweredNo(this string answer) + => !answer.AnsweredYes(); + + public static Process StartProcess(ProcessStartInfo info) + => Process.Start(info) + ?? throw new Exception($"Failed to start process \"{info.FileName}\""); +} \ No newline at end of file diff --git a/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/Resources/barotrauma.icns b/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/Resources/barotrauma.icns new file mode 100644 index 000000000..688e9ecef Binary files /dev/null and b/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/Resources/barotrauma.icns differ diff --git a/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/info.plist b/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/info.plist new file mode 100644 index 000000000..d4a6b7a47 --- /dev/null +++ b/Deploy/DeployAll/macSkeleton/Barotrauma.app/Contents/info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + Barotrauma + CFBundleIconFile + barotrauma + CFBundleIdentifier + com.FakeFish.Barotrauma + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Barotrauma + CFBundlePackageType + APPL + CFBundleShortVersionString + {short_version_string} + CFBundleSignature + FONV + CFBundleVersion + {version} + LSApplicationCategoryType + public.app-category.games + LSMinimumSystemVersion + 10.13 + NSMicrophoneUsageDescription + Needs microphone access for in-game communications + NSHumanReadableCopyright + Copyright © 2017-{current_year} FakeFish. All rights reserved. + NSPrincipalClass + NSApplication + + diff --git a/Deploy/DeployAll/macSkeleton/Barotrauma.app/installscript.vdf b/Deploy/DeployAll/macSkeleton/Barotrauma.app/installscript.vdf new file mode 100644 index 000000000..e9e1eccb6 --- /dev/null +++ b/Deploy/DeployAll/macSkeleton/Barotrauma.app/installscript.vdf @@ -0,0 +1,31 @@ +"InstallScript" +{ + "version" "2" + "chmod" + { + "0" + { + "file" "Barotrauma.app/Contents/MacOS/Barotrauma" + "mode" "755" + } + "1" + { + "file" "Barotrauma.app/Contents/MacOS/Barotrauma.bin.osx" + "mode" "755" + } + "2" + { + "file" "Barotrauma.app/Contents/MacOS/DedicatedServer" + "mode" "755" + } + "3" + { + "file" "Barotrauma.app/Contents/MacOS/DedicatedServer.bin.osx" + "mode" "755" + } + } +} +"kvsignatures" +{ + "InstallScript" "2d0e72227a48d72bfda9810a7f5478af6670d8653f25a92974ceaa4d2e1009e0f5e8a5312a092a64790635d574b0adb84a265ee89df71470d7e8e15c915420da429eb4a15d4ee68840d19c8928a970ab25b8bbfb13f22ce3a061bbb604a94f92299d6e94d7543f3f7bd51170a4c31b3f9808f2f98e85ffd4bd074e88da44491e" +} diff --git a/Deploy/Linux/DeployLinux.sh b/Deploy/Linux/DeployLinux.sh deleted file mode 100755 index c9f7e721b..000000000 --- a/Deploy/Linux/DeployLinux.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployLinuxServer.sh b/Deploy/Linux/DeployLinuxServer.sh deleted file mode 100644 index 4d05a7814..000000000 --- a/Deploy/Linux/DeployLinuxServer.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma/BarotraumaServer -dotnet publish LinuxServer.csproj -c Release --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployLinuxUnstable.sh b/Deploy/Linux/DeployLinuxUnstable.sh deleted file mode 100755 index 015ae2cc7..000000000 --- a/Deploy/Linux/DeployLinuxUnstable.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployMac.sh b/Deploy/Linux/DeployMac.sh deleted file mode 100644 index 0bde38073..000000000 --- a/Deploy/Linux/DeployMac.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish MacClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish MacServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployMacUnstable.sh b/Deploy/Linux/DeployMacUnstable.sh deleted file mode 100644 index ecccfe3fc..000000000 --- a/Deploy/Linux/DeployMacUnstable.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish MacClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish MacServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployWindows.sh b/Deploy/Linux/DeployWindows.sh deleted file mode 100644 index 527841ed4..000000000 --- a/Deploy/Linux/DeployWindows.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployWindowsUnstable.sh b/Deploy/Linux/DeployWindowsUnstable.sh deleted file mode 100644 index ccabb7aee..000000000 --- a/Deploy/Linux/DeployWindowsUnstable.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" - -cd .. -cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" diff --git a/Deploy/ManualScripts/Linux/DeployLinux.sh b/Deploy/ManualScripts/Linux/DeployLinux.sh new file mode 100644 index 000000000..9ea5ba91f --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployLinux.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish LinuxClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish LinuxServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployLinuxServer.sh b/Deploy/ManualScripts/Linux/DeployLinuxServer.sh new file mode 100644 index 000000000..5e49f9654 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployLinuxServer.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd ../../../Barotrauma/BarotraumaServer +dotnet publish LinuxServer.csproj -c Release --self-contained -r linux-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployLinuxUnstable.sh b/Deploy/ManualScripts/Linux/DeployLinuxUnstable.sh new file mode 100644 index 000000000..b2c270ee2 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployLinuxUnstable.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish LinuxClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish LinuxServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployMac.sh b/Deploy/ManualScripts/Linux/DeployMac.sh new file mode 100644 index 000000000..66592c074 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployMac.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish MacClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish MacServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployMacUnstable.sh b/Deploy/ManualScripts/Linux/DeployMacUnstable.sh new file mode 100644 index 000000000..1bea5df67 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployMacUnstable.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish MacClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish MacServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployWindows.sh b/Deploy/ManualScripts/Linux/DeployWindows.sh new file mode 100644 index 000000000..4cb05dc21 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployWindows.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish WindowsClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish WindowsServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/Linux/DeployWindowsUnstable.sh b/Deploy/ManualScripts/Linux/DeployWindowsUnstable.sh new file mode 100644 index 000000000..f18b284f3 --- /dev/null +++ b/Deploy/ManualScripts/Linux/DeployWindowsUnstable.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish WindowsClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish WindowsServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" \/p:RollForward=Disable \/p:RuntimeFrameworkVersion=3.1.16 diff --git a/Deploy/ManualScripts/SteamPipeBuildScripts/app_1026340.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/app_1026340.vdf new file mode 100644 index 000000000..70d3550a6 --- /dev/null +++ b/Deploy/ManualScripts/SteamPipeBuildScripts/app_1026340.vdf @@ -0,0 +1,15 @@ +"appbuild" +{ + "appid" "1026340" + "desc" "" + "buildoutput" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\output" + "contentroot" "" + "setlive" "" + "preview" "0" + "local" "" + "depots" + { + "1026341" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_1026341.vdf" + "1026342" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_1026342.vdf" + } +} diff --git a/Deploy/ManualScripts/SteamPipeBuildScripts/app_602960.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/app_602960.vdf new file mode 100644 index 000000000..f2fca8b7f --- /dev/null +++ b/Deploy/ManualScripts/SteamPipeBuildScripts/app_602960.vdf @@ -0,0 +1,16 @@ +"appbuild" +{ + "appid" "602960" + "desc" "" + "buildoutput" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\output" + "contentroot" "" + "setlive" "" + "preview" "0" + "local" "" + "depots" + { + "602961" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602961.vdf" + "602962" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602962.vdf" + "602963" "[steamworks_sdk_install_folder]\sdk\tools\ContentBuilder\scripts\depot_602963.vdf" + } +} \ No newline at end of file diff --git a/BuildScripts/depot_1026341.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/depot_1026341.vdf similarity index 100% rename from BuildScripts/depot_1026341.vdf rename to Deploy/ManualScripts/SteamPipeBuildScripts/depot_1026341.vdf diff --git a/BuildScripts/depot_1026342.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/depot_1026342.vdf similarity index 100% rename from BuildScripts/depot_1026342.vdf rename to Deploy/ManualScripts/SteamPipeBuildScripts/depot_1026342.vdf diff --git a/BuildScripts/depot_602961.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/depot_602961.vdf similarity index 100% rename from BuildScripts/depot_602961.vdf rename to Deploy/ManualScripts/SteamPipeBuildScripts/depot_602961.vdf diff --git a/BuildScripts/depot_602962.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/depot_602962.vdf similarity index 100% rename from BuildScripts/depot_602962.vdf rename to Deploy/ManualScripts/SteamPipeBuildScripts/depot_602962.vdf diff --git a/BuildScripts/depot_602963.vdf b/Deploy/ManualScripts/SteamPipeBuildScripts/depot_602963.vdf similarity index 100% rename from BuildScripts/depot_602963.vdf rename to Deploy/ManualScripts/SteamPipeBuildScripts/depot_602963.vdf diff --git a/Deploy/ManualScripts/Windows/DeployLinux.bat b/Deploy/ManualScripts/Windows/DeployLinux.bat new file mode 100644 index 000000000..ee2f601e4 --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployLinux.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish LinuxClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish LinuxServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployLinuxUnstable.bat b/Deploy/ManualScripts/Windows/DeployLinuxUnstable.bat new file mode 100644 index 000000000..39d2e158f --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployLinuxUnstable.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish LinuxClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish LinuxServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployMac.bat b/Deploy/ManualScripts/Windows/DeployMac.bat new file mode 100644 index 000000000..7c182a920 --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployMac.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish MacClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish MacServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployMacUnstable.bat b/Deploy/ManualScripts/Windows/DeployMacUnstable.bat new file mode 100644 index 000000000..0ceea7daa --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployMacUnstable.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish MacClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish MacServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployWindows.bat b/Deploy/ManualScripts/Windows/DeployWindows.bat new file mode 100644 index 000000000..3bbdf4829 --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployWindows.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish WindowsClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish WindowsServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployWindowsServer.bat b/Deploy/ManualScripts/Windows/DeployWindowsServer.bat new file mode 100644 index 000000000..02beb909a --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployWindowsServer.bat @@ -0,0 +1,6 @@ +@ECHO OFF + +cd ../../../Barotrauma/BarotraumaServer +dotnet publish WindowsServer.csproj -c Release --self-contained -r win-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/ManualScripts/Windows/DeployWindowsUnstable.bat b/Deploy/ManualScripts/Windows/DeployWindowsUnstable.bat new file mode 100644 index 000000000..7864badb0 --- /dev/null +++ b/Deploy/ManualScripts/Windows/DeployWindowsUnstable.bat @@ -0,0 +1,12 @@ +@ECHO OFF + +cd ../../../Barotrauma + +cd BarotraumaClient +dotnet publish WindowsClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +cd .. +cd BarotraumaServer +dotnet publish WindowsServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 /p:RollForward=Disable /p:RuntimeFrameworkVersion=3.1.16 + +PAUSE diff --git a/Deploy/Windows/DeployLinux.bat b/Deploy/Windows/DeployLinux.bat deleted file mode 100644 index 13cebab0c..000000000 --- a/Deploy/Windows/DeployLinux.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployLinuxUnstable.bat b/Deploy/Windows/DeployLinuxUnstable.bat deleted file mode 100644 index ce7c60bc5..000000000 --- a/Deploy/Windows/DeployLinuxUnstable.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployMac.bat b/Deploy/Windows/DeployMac.bat deleted file mode 100644 index c1488f7a3..000000000 --- a/Deploy/Windows/DeployMac.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish MacClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish MacServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployMacUnstable.bat b/Deploy/Windows/DeployMacUnstable.bat deleted file mode 100644 index 771f83526..000000000 --- a/Deploy/Windows/DeployMacUnstable.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish MacClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish MacServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployWindows.bat b/Deploy/Windows/DeployWindows.bat deleted file mode 100644 index cdd63185f..000000000 --- a/Deploy/Windows/DeployWindows.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployWindowsServer.bat b/Deploy/Windows/DeployWindowsServer.bat deleted file mode 100644 index 865d61d03..000000000 --- a/Deploy/Windows/DeployWindowsServer.bat +++ /dev/null @@ -1,6 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma/BarotraumaServer -dotnet publish WindowsServer.csproj -c Release --self-contained -r win-x64 /p:Platform=x64 - -PAUSE diff --git a/Deploy/Windows/DeployWindowsUnstable.bat b/Deploy/Windows/DeployWindowsUnstable.bat deleted file mode 100644 index d8d4b4f7e..000000000 --- a/Deploy/Windows/DeployWindowsUnstable.bat +++ /dev/null @@ -1,12 +0,0 @@ -@ECHO OFF - -cd ../../Barotrauma - -cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 - -cd .. -cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 - -PAUSE diff --git a/Libraries/Facepunch.Steamworks/ServerList/Base.cs b/Libraries/Facepunch.Steamworks/ServerList/Base.cs index 9a6868702..9a5cb1125 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Base.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Base.cs @@ -23,17 +23,17 @@ namespace Steamworks.ServerList /// /// When a new server is added, this function will get called /// - public event Action OnChanges; + public Action OnChanges; /// /// Called for every responsive server /// - public event Action OnResponsiveServer; + public Action OnResponsiveServer; /// /// Called for every unresponsive server /// - public event Action OnUnresponsiveServer; + public Action OnUnresponsiveServer; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -65,14 +65,14 @@ namespace Steamworks.ServerList var thisRequest = request; - while ( IsRefreshing ) + while ( true ) { await Task.Delay( 33 ); // // The request has been cancelled or changed in some way // - if ( request.Value == IntPtr.Zero || thisRequest.Value != request.Value ) + if ( request.Value == IntPtr.Zero || thisRequest.Value != request.Value || !IsRefreshing ) return false; if ( !SteamClient.IsValid ) diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 0561b8dd5..510227bbf 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -114,9 +114,9 @@ namespace Steamworks public struct FriendGameInfo { - internal ulong GameID; // m_gameID class CGameID - internal uint GameIP; // m_unGameIP uint32 - internal ulong SteamIDLobby; // m_steamIDLobby class CSteamID + public ulong GameID; // m_gameID class CGameID + public uint GameIP; // m_unGameIP uint32 + public ulong SteamIDLobby; // m_steamIDLobby class CSteamID public int ConnectionPort; public int QueryPort; diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 1a3b3fb2d..2b0fdb57a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -153,13 +153,13 @@ namespace Steamworks.Data } /// - /// Refreshes metadata for a lobby you're not necessarily in right now - /// you never do this for lobbies you're a member of, only if your - /// this will send down all the metadata associated with a lobby - /// this is an asynchronous call - /// returns false if the local user is not connected to the Steam servers - /// results will be returned by a LobbyDataUpdate_t callback - /// if the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false + /// Refreshes metadata for a lobby you're not necessarily in right now. + /// You never do this for lobbies you're a member of, only if your + /// this will send down all the metadata associated with a lobby. + /// This is an asynchronous call. + /// Returns false if the local user is not connected to the Steam servers. + /// Results will be returned by a LobbyDataUpdate_t callback. + /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false. /// public bool Refresh() { diff --git a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj index aab701817..6b99d030a 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/LinuxSolution.sln b/LinuxSolution.sln index e970112ea..7d8c2e248 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -44,6 +44,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\Bar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{382DFA63-78FC-41AC-BA85-630960A56E5C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 @@ -201,18 +203,18 @@ Global {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|Any CPU.Build.0 = Debug|Any CPU {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.ActiveCfg = Debug|Any CPU {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.Build.0 = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Debug|x64.ActiveCfg = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Debug|x64.Build.0 = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Release|Any CPU.Build.0 = Release|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Release|x64.ActiveCfg = Release|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Release|x64.Build.0 = Release|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Unstable|Any CPU.Build.0 = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Unstable|x64.ActiveCfg = Debug|Any CPU - {382DFA63-78FC-41AC-BA85-630960A56E5C}.Unstable|x64.Build.0 = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Debug|x64.ActiveCfg = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Debug|x64.Build.0 = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Release|Any CPU.Build.0 = Release|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Release|x64.ActiveCfg = Release|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Release|x64.Build.0 = Release|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|x64.ActiveCfg = Debug|Any CPU + {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,7 +233,7 @@ Global {2B0881F6-9C67-4446-A1F2-FC042763A462} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {33E95A21-E071-4432-819F-AA64CF3EF3F1} = {DE36F45F-F09E-4719-B953-00D148F7722A} {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} - {382DFA63-78FC-41AC-BA85-630960A56E5C} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {60B82E13-2CDD-4C74-8373-FD7264D6C80B} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/MacSolution.sln b/MacSolution.sln index f3f6acae3..af967aaf9 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -32,6 +32,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacServer", "Barotrauma\BarotraumaServer\MacServer.csproj", "{8C3F4314-E5CA-4563-BEE6-69E97CAA0813}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacClient", "Barotrauma\BarotraumaClient\MacClient.csproj", "{F17FB469-E9E6-4B1C-B887-4FE709D4D771}" + ProjectSection(ProjectDependencies) = postProject + {8C3F4314-E5CA-4563-BEE6-69E97CAA0813} = {8C3F4314-E5CA-4563-BEE6-69E97CAA0813} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.MacOS.NetStandard", "Libraries\MonoGame.Framework\Src\MonoGame.Framework\MonoGame.Framework.MacOS.NetStandard.csproj", "{35DDDA7D-328D-4A5D-BCBB-2E60C830A899}" EndProject @@ -41,6 +44,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\Barot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{40BDE83D-61D5-481C-A53E-E0F5B23881E2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{c54f0dfe-add3-4767-8cbc-101859218d66}*SharedItemsImports = 5 @@ -198,18 +203,18 @@ Global {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|Any CPU.Build.0 = Debug|Any CPU {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.ActiveCfg = Debug|Any CPU {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.Build.0 = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Debug|x64.ActiveCfg = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Debug|x64.Build.0 = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Release|Any CPU.Build.0 = Release|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Release|x64.ActiveCfg = Release|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Release|x64.Build.0 = Release|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Unstable|Any CPU.Build.0 = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Unstable|x64.ActiveCfg = Debug|Any CPU - {40BDE83D-61D5-481C-A53E-E0F5B23881E2}.Unstable|x64.Build.0 = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Debug|x64.Build.0 = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Release|Any CPU.Build.0 = Release|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Release|x64.ActiveCfg = Release|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Release|x64.Build.0 = Release|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|x64.ActiveCfg = Debug|Any CPU + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -228,7 +233,7 @@ Global {35DDDA7D-328D-4A5D-BCBB-2E60C830A899} = {DE36F45F-F09E-4719-B953-00D148F7722A} {F10CE3BB-26B8-446E-84D2-86D25E850F61} = {DE36F45F-F09E-4719-B953-00D148F7722A} {20BC9336-B439-4BF1-8B65-D587DBF421D1} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} - {40BDE83D-61D5-481C-A53E-E0F5B23881E2} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {36B38D18-3574-4B67-A89C-FD3C2D39F1D6} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/README.md b/README.md index 63c1f2398..b6e4a37d0 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ If you're interested in working on the code, either to develop mods or to contri ## Prerequisities: ### Windows -- [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 8.0 support (VS 2019 or later recommended) +- [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 10 support (VS 2022 or later recommended) ### Linux -- [.NET Core 3.1 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) +- [.NET 6 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux) ### macOS -- [Visual Studio 2019 for Mac](https://visualstudio.microsoft.com/vs/mac/) +- [Visual Studio 2022 for Mac](https://visualstudio.microsoft.com/vs/mac/) diff --git a/WindowsSolution.sln b/WindowsSolution.sln index efba0f9be..7f6b6b376 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -44,6 +44,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsTest", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 @@ -201,18 +203,12 @@ Global {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|Any CPU.Build.0 = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Debug|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|x64.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|x64.Build.0 = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|Any CPU.Build.0 = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|x64.ActiveCfg = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|x64.Build.0 = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|Any CPU.Build.0 = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|x64.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|x64.Build.0 = Debug|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.Build.0 = Debug|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.ActiveCfg = Release|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.Build.0 = Release|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Debug|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,7 +227,7 @@ Global {1F318AC4-F808-4130-867F-B98DF9AA8F95} = {DE36F45F-F09E-4719-B953-00D148F7722A} {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {C98FE0D0-BC7D-4806-B592-734B53016FD8} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A}