diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index db7c27941..02ab56891 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 323184f04..a605b41c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -173,8 +173,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; } @@ -491,7 +491,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); @@ -538,6 +538,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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 49292f426..6f32c617c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -175,6 +175,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()) @@ -534,6 +539,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); @@ -569,7 +575,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) { @@ -650,7 +656,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; @@ -667,7 +673,14 @@ namespace Barotrauma } } - character.Enabled = Controlled == character || enabled; + if (disabledByEvent) + { + character.DisabledByEvent = true; + } + else + { + character.Enabled = Controlled == character || enabled; + } return character; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 19dbd141f..b5acfe2e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -1461,7 +1461,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; @@ -1503,7 +1503,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) { @@ -1586,7 +1586,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; @@ -1604,7 +1604,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; } @@ -1620,10 +1620,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) @@ -1686,7 +1686,7 @@ namespace Barotrauma if (!affliction.ShouldShowIcon(Character)) { continue; } if (!affliction.Prefab.IsBuff) { - negativeEffect += affliction.Strength; + negativeEffect += affliction.Strength * GetVitalityMultiplier(affliction, limbHealth); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 2eee53707..22196a3e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -226,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": @@ -429,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; @@ -2849,7 +2831,7 @@ namespace Barotrauma ); AssignOnClientExecute( - "banendpoint|banip", + "banaddress|banip", (string[] args) => { if (GameMain.Client == null || args.Length == 0) return; @@ -2871,7 +2853,7 @@ namespace Barotrauma } GameMain.Client?.SendConsoleCommand( - "banendpoint " + + "banaddress " + args[0] + " " + (banDuration.HasValue ? banDuration.Value.TotalSeconds.ToString() : "0") + " " + reason); @@ -2884,13 +2866,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 f216984a4..e8bddc5f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -63,33 +63,43 @@ 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); + } + } + + List extraButtons = CreateConversation(conversationList, text, speaker, options, string.IsNullOrWhiteSpace(spriteIdentifier)); + AssignActionsToButtons(extraButtons, lastMessageBox); + RecalculateLastMessage(conversationList, true); + + conversationList.ScrollToEnd(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 +110,10 @@ namespace Barotrauma { UserData = "ConversationAction" }; - + messageBox.OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (!(Screen.Selected is GameScreen)) { messageBox.Close(); } + }; lastMessageBox = messageBox; messageBox.InnerFrame.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index c4ec84e16..80de95f63 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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 63021ce65..4e4f7c94d 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; @@ -981,7 +981,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index dff86c500..61ddb8f50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -806,6 +806,13 @@ namespace Barotrauma flashColor = (color == null) ? GUIStyle.Red : (Color)color; } + public void ImmediateFlash(Color? color = null) + { + 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) { CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait)); @@ -1156,7 +1163,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/GUIDragHandle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs index b4987a87d..678b3fb29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDragHandle.cs @@ -7,68 +7,88 @@ namespace Barotrauma { private readonly RectTransform elementToMove; + private Point originalOffset; + private Vector2 dragStart; private bool dragStarted; public Rectangle DragArea; + public Func ValidatePosition; + 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; + if (!Visible) { return; } base.Update(deltaTime); - Enabled = true; - if (dragStarted) + + if (enabled) { - Point moveAmount = (PlayerInput.MousePosition - dragStart).ToPoint() - elementToMove.ScreenSpaceOffset; - Rectangle rect = elementToMove.Rect; - rect.Location += moveAmount; + 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); + 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; - } + if (moveAmount != Point.Zero) + { + elementToMove.ScreenSpaceOffset += moveAmount; + } - if (!PlayerInput.PrimaryMouseButtonHeld()) - { - dragStarted = false; + 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)) - { - State = Selected ? ComponentState.HoverSelected : ComponentState.Hover; - if (PlayerInput.PrimaryMouseButtonDown()) + else if (Rect.Contains(PlayerInput.MousePosition) && CanBeFocused && Enabled && GUI.IsMouseOn(this) && !(GUI.MouseOn is GUIButton)) { - dragStart = PlayerInput.MousePosition - elementToMove.ScreenSpaceOffset.ToVector2(); - dragStarted = true; - } - } - else - { - if (!ExternalHighlight) - { - State = Selected ? ComponentState.Selected : ComponentState.None; + State = Selected ? ComponentState.HoverSelected : ComponentState.Hover; + if (PlayerInput.PrimaryMouseButtonDown()) + { + originalOffset = elementToMove.ScreenSpaceOffset; + dragStart = PlayerInput.MousePosition - elementToMove.ScreenSpaceOffset.ToVector2(); + dragStarted = true; + } } else { - State = ComponentState.Hover; + 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/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 8d8a563b4..8936fb126 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -448,7 +448,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,7 +471,7 @@ namespace Barotrauma public void SetBackgroundIcon(Sprite icon) { if (icon == null) { return; } - if (icon == BackgroundIcon.Sprite) { return; } + if (icon == BackgroundIcon?.Sprite) { return; } GUIImage newIcon = new GUIImage(new RectTransform(icon.size.ToPoint(), RectTransform), icon) { IgnoreLayoutGroups = true, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 866214dd6..27a558c5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -402,7 +402,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()) @@ -438,10 +439,6 @@ namespace Barotrauma isSelecting = false; State = ComponentState.None; } - if (!isSelecting) - { - isSelecting = PlayerInput.IsShiftDown(); - } if (mouseHeldInside && !PlayerInput.PrimaryMouseButtonHeld()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index aded19429..1e7d1cd03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -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++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 7887ea6db..fc4cc62f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1313,6 +1313,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; 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 f0930992a..b1dcac1fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -109,9 +109,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; @@ -232,20 +230,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); @@ -595,7 +587,7 @@ namespace Barotrauma { try { - ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand), out ConnectName, out ConnectEndpoint, out ConnectLobby); + ConnectCommand = ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand)); } catch (IndexOutOfRangeException e) { @@ -604,12 +596,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,38 +722,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); @@ -1082,7 +1061,7 @@ namespace Barotrauma if (Client != null) { - Client.Disconnect(); + Client.Quit(); Client = null; } @@ -1162,7 +1141,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); } @@ -1200,7 +1179,7 @@ namespace Barotrauma { exiting = true; DebugConsole.NewMessage("Exiting..."); - NetworkMember?.Disconnect(); + NetworkMember?.Quit(); SteamManager.ShutDown(); try diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 07ed155ff..bbdde97f2 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; } } @@ -143,7 +143,7 @@ namespace Barotrauma 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)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 73ea45844..7614a8b8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1390,7 +1390,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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 60814bd1c..06f1d24df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -132,9 +132,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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 4db03f290..1d1b488d3 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')); } @@ -141,7 +141,7 @@ namespace Barotrauma { 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(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index c60171bfe..ebbc45e0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -148,6 +148,8 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } + public bool LockGuiFramePosition; + [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap { @@ -586,8 +588,58 @@ namespace Barotrauma.Items.Components }; int iconHeight = GUIStyle.ItemFrameMargin.Y / 4; - new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), handle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, + var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), handle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, style: "GUIDragIndicatorHorizontal"); + + 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; + } + } + 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 / 10) }, + style: "GUIButtonRefresh") + { + 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; + } + }; } } @@ -648,6 +700,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/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 1eeedca4b..1c4dfe80b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -255,7 +255,7 @@ namespace Barotrauma.Items.Components foreach (DeconstructItem deconstructItem in it.Prefab.DeconstructItems) { - RegisterItem(deconstructItem.ItemIdentifier); + RegisterItem(deconstructItem.ItemIdentifier, deconstructItem.Amount); } if (it.OwnInventory is { } inventory) @@ -266,15 +266,14 @@ namespace Barotrauma.Items.Components } } - void RegisterItem(Identifier identifier) + void RegisterItem(Identifier identifier, int amount = 1) { if (itemCounts.ContainsKey(identifier)) { - itemCounts[identifier]++; + itemCounts[identifier] += amount; return; } - - itemCounts.Add(identifier, 1); + itemCounts.Add(identifier, amount); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index cfe675dbb..5e973af30 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 { @@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components 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; } @@ -76,6 +79,20 @@ 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()) + { + string textureDir = GetTextureDirectory(subElement); + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "temperatureboostsoundup": + temperatureBoostSoundUp = RoundSound.Load(subElement, false); + break; + case "temperatureboostsounddown": + temperatureBoostSoundDown = RoundSound.Load(subElement, false); + break; + } + } + paddedFrame = new GUILayoutGroup(new RectTransform( GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, @@ -354,7 +371,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)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 20fc3437e..c67963162 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -354,7 +354,7 @@ 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 8d5d16766..fe6ba3270 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -317,6 +317,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/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/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index b64fda77a..7858e3154 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -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) { @@ -1444,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) { @@ -1453,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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 7431e8dda..8547be8c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -569,6 +569,36 @@ 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); + 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()) @@ -976,7 +1006,7 @@ 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(); @@ -991,6 +1021,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 +1046,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) @@ -1239,6 +1266,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 864384257..cedbfbf72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -252,7 +252,7 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) placePosition = position; + if (PlayerInput.PrimaryMouseButtonHeld() && GUI.MouseOn == null) { placePosition = position; } } else { @@ -270,11 +270,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 +281,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/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 b07a40828..c7a818db0 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))) @@ -738,15 +741,6 @@ namespace Barotrauma /// public static void DrawSelecting(SpriteBatch spriteBatch, Camera cam) { - if (Screen.Selected is SubEditorScreen subEditor) - { - if (subEditor.IsMouseOnEditorGUI()) { return; } - } - else if (GUI.MouseOn != null) - { - return; - } - Vector2 position = PlayerInput.MousePosition; position = cam.ScreenToWorld(position); @@ -819,7 +813,7 @@ namespace Barotrauma selectionPos = Vector2.Zero; } } - if (selectionPos != null && selectionPos != Vector2.Zero) + if (selectionPos != Vector2.Zero) { var (sizeX, sizeY) = selectionSize; var (posX, posY) = selectionPos; 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/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index ee708d2ef..9ff0f0d9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -27,7 +27,7 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) + if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) placePosition = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); newRect.X = (int)position.X; 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..23afe3580 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -1,28 +1,28 @@ -using Barotrauma.Steam; -using Microsoft.Xna.Framework; +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; @@ -31,8 +31,7 @@ namespace Barotrauma.Networking get { return banFrame; } } - public List localRemovedBans = new List(); - public List localRangeBans = new List(); + public List localRemovedBans = new List(); private void RecreateBanFrame() { @@ -71,28 +70,22 @@ 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 ? @@ -127,19 +120,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 +139,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,19 +152,30 @@ 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) @@ -198,20 +188,13 @@ namespace Barotrauma.Networking public void ClientAdminWrite(IWriteMessage outMsg) { - outMsg.Write((UInt16)localRemovedBans.Count); - foreach (UInt16 uniqueId in localRemovedBans) - { - outMsg.Write(uniqueId); - } - - outMsg.Write((UInt16)localRangeBans.Count); - foreach (UInt16 uniqueId in localRangeBans) + outMsg.WriteVariableUInt32((UInt32)localRemovedBans.Count); + foreach (UInt32 uniqueId in localRemovedBans) { outMsg.Write(uniqueId); } localRemovedBans.Clear(); - localRangeBans.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index a020b263a..ec7909d75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -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..480fa90cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -72,8 +72,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1a0efc16c..59be7ad9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -206,7 +206,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 +329,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 @@ -406,7 +406,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 + "\"."); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 532928c46..9a04ff9e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1,18 +1,16 @@ -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 { @@ -23,14 +21,9 @@ namespace Barotrauma.Networking get { return true; } } - private string name; - private UInt16 nameId = 0; - public string Name - { - get { return name; } - } + public string Name { get; private set; } public string PendingName = string.Empty; @@ -38,7 +31,7 @@ namespace Barotrauma.Networking { value = value.Replace(":", "").Replace(";", ""); if (string.IsNullOrEmpty(value)) { return; } - name = value; + Name = value; nameId++; } @@ -89,13 +82,11 @@ 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; @@ -129,10 +120,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 +128,7 @@ namespace Barotrauma.Networking private set; } - public override List ConnectedClients + public override IReadOnlyList ConnectedClients { get { @@ -175,14 +163,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,11 +182,10 @@ 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; @@ -280,7 +263,7 @@ namespace Barotrauma.Networking fileReceiver.OnFinished += OnFileReceived; fileReceiver.OnTransferFailed += OnTransferFailed; - characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, name, null) + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, Name, originalName: null) { Job = null }; @@ -290,15 +273,8 @@ namespace Barotrauma.Networking serverSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); Voting = new Voting(); - if (steamId == 0) - { - serverEndpoint = ip; - } - else - { - serverEndpoint = steamId; - } - ConnectToServer(serverEndpoint, serverName); + serverEndpoint = endpoint; + InitiateServerJoin(serverName); //ServerLog = new ServerLog(""); @@ -306,7 +282,7 @@ namespace Barotrauma.Networking GameMain.ResetNetLobbyScreen(); } - private void ConnectToServer(object endpoint, string hostName) + 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,48 @@ 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; - } + clientPeer = CreateNetPeer(); + clientPeer.Start(); updateInterval = new TimeSpan(0, 0, 0, 0, 150); CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } + private ClientPeer CreateNetPeer() + { + Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks( + ReadDataMessage, + OnClientPeerDisconnect, + HandleDisconnectMessage, + (int salt, int retries) => + { + if (pwRetries != retries) + { + wrongPassword = retries > 0; + requiresPw = true; + } + pwRetries = retries; + }, + 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; @@ -442,7 +364,7 @@ namespace Barotrauma.Networking { ChildServerRelay.ShutDown(); connectCancelled = true; - Disconnect(); + Quit(); } private bool wrongPassword; @@ -467,14 +389,13 @@ namespace Barotrauma.Networking { if (reconnectBox == null && waitInServerQueueBox == null) { - string serverDisplayName = serverName; - if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = serverIP; } + 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; @@ -604,7 +525,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); @@ -645,7 +566,7 @@ 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(); + Quit(); GameMain.ServerListScreen.Select(); return; } @@ -688,7 +609,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); @@ -769,7 +690,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; @@ -1079,16 +1000,15 @@ 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(bool disableReconnect) { CoroutineManager.StopCoroutines("WaitForStartingInfo"); reconnectBox?.Close(); reconnectBox = null; - GameMain.ModDownloadScreen.Reset(); - ContentPackageManager.EnabledPackages.Restore(); - GUI.ClearCursorWait(); if (disableReconnect) { allowReconnect = false; } @@ -1125,7 +1045,6 @@ namespace Barotrauma.Networking if (disconnectReason != DisconnectReason.Banned && disconnectReason != DisconnectReason.ServerShutdown && disconnectReason != DisconnectReason.TooManyFailedLogins && - disconnectReason != DisconnectReason.NotOnWhitelist && disconnectReason != DisconnectReason.MissingContentPackage && disconnectReason != DisconnectReason.InvalidVersion) { @@ -1197,14 +1116,16 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = new GUIMessageBox( TextManager.Get("ConnectionLost"), msg, - new LocalizedString[] { TextManager.Get("Cancel") }); - reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; + new LocalizedString[] { TextManager.Get("Cancel") }) + { + DisplayInLoadingScreens = true + }; + reconnectBox.Buttons[0].OnClicked += ReturnToPreviousMenu; connected = false; - var prevContentPackages = clientPeer.ServerContentPackages; //decrement lobby update ID to make sure we update the lobby when we reconnect GameMain.NetLobbyScreen.LastUpdateID--; - ConnectToServer(serverEndpoint, serverName); + 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 @@ -1243,12 +1164,18 @@ namespace Barotrauma.Networking 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")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer")) + { + DisplayInLoadingScreens = true + }; msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } else { - var msgBox = new GUIMessageBox(TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer"), msg); + var msgBox = new GUIMessageBox(TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer"), msg) + { + DisplayInLoadingScreens = true + }; msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } @@ -1266,8 +1193,8 @@ namespace Barotrauma.Networking if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); - Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); + Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + ServerName); + Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + ServerName.Replace("\"", "\\\"") + "\" " + serverEndpoint); } canStart = true; @@ -1275,7 +1202,7 @@ namespace Barotrauma.Networking VoipClient = new VoipClient(this, clientPeer); - if (Screen.Selected != GameMain.GameScreen) + if (Screen.Selected != GameMain.GameScreen && !(Screen.Selected is RoundSummaryScreen)) { GameMain.ModDownloadScreen.Select(); } @@ -1312,7 +1239,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 +1310,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])); } @@ -1923,7 +1850,7 @@ namespace Barotrauma.Networking private void ReadInitialUpdate(IReadMessage inc) { - myID = inc.ReadByte(); + SessionId = inc.ReadByte(); UInt16 subListCount = inc.ReadUInt16(); ServerSubmarines.Clear(); @@ -1983,22 +1910,22 @@ 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; @@ -2009,22 +1936,22 @@ namespace Barotrauma.Networking 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 +1962,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) @@ -2405,7 +2332,7 @@ namespace Barotrauma.Networking outmsg.Write(ChatMessage.LastID); outmsg.Write(LastClientListUpdateID); outmsg.Write(nameId); - outmsg.Write(name); + outmsg.Write(Name); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; if (jobPreferences.Count > 0) { @@ -2519,7 +2446,7 @@ namespace Barotrauma.Networking 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); @@ -2761,7 +2688,7 @@ namespace Barotrauma.Networking return false; } - public override void Disconnect() + public override void Quit() { allowReconnect = false; @@ -2770,6 +2697,9 @@ namespace Barotrauma.Networking SteamManager.LeaveLobby(); } + GameMain.ModDownloadScreen.Reset(); + ContentPackageManager.EnabledPackages.Restore(); + CampaignMode.StartRoundCancellationToken?.Cancel(); clientPeer?.Close(); @@ -2864,7 +2794,7 @@ namespace Barotrauma.Networking public void VoteForKick(Client votedClient) { if (votedClient == null) { return; } - votedClient.AddKickVote(ConnectedClients.FirstOrDefault(c => c.ID == myID)); + votedClient.AddKickVote(ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId)); Vote(VoteType.Kick, votedClient); } @@ -2918,26 +2848,35 @@ namespace Barotrauma.Networking 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 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); + msg.Write(true); msg.WritePadBits(); + msg.Write(playerName); + clientPeer.Send(msg, DeliveryMethod.Reliable); + } + + public override void UnbanPlayer(Endpoint endpoint) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); + msg.Write((UInt16)ClientPermissions.Unban); + msg.Write(false); msg.WritePadBits(); + msg.Write(endpoint.StringRepresentation); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3470,7 +3409,7 @@ namespace Barotrauma.Networking public virtual 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; } @@ -3543,12 +3482,12 @@ namespace Barotrauma.Networking }; if (GameMain.NetworkMember.ConnectedClients != null) { - kickVoteButton.Enabled = !client.HasKickVoteFromID(myID); + kickVoteButton.Enabled = !client.HasKickVoteFromSessionId(SessionId); } } } - 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"), @@ -3609,11 +3548,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 @@ -3626,7 +3565,7 @@ 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); @@ -3634,12 +3573,12 @@ namespace Barotrauma.Networking switch (error) { case ClientNetError.MISSING_EVENT: - outMsg.Write(expectedID); - outMsg.Write(eventID); + outMsg.Write(expectedId); + outMsg.Write(eventId); break; case ClientNetError.MISSING_ENTITY: - outMsg.Write(eventID); - outMsg.Write(entityID); + outMsg.Write(eventId); + outMsg.Write(entityId); outMsg.Write((byte)Submarine.Loaded.Count); foreach (Submarine sub in Submarine.Loaded) { @@ -3651,7 +3590,7 @@ namespace Barotrauma.Networking if (!eventErrorWritten) { - WriteEventErrorData(error, expectedID, eventID, entityID); + WriteEventErrorData(error, expectedId, eventId, entityId); eventErrorWritten = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 50486ace0..929701014 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -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/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index f39baaaec..b7aa17ec1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -1,11 +1,9 @@ -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; namespace Barotrauma.Networking { @@ -18,7 +16,7 @@ namespace Barotrauma.Networking public readonly UInt64 WorkshopId; public readonly DateTime InstallTime; - public RegularPackage RegularPackage + public RegularPackage? RegularPackage { get { @@ -26,7 +24,7 @@ namespace Barotrauma.Networking } } - public CorePackage CorePackage + public CorePackage? CorePackage { get { @@ -34,8 +32,8 @@ namespace Barotrauma.Networking } } - public ContentPackage ContentPackage - => (ContentPackage)RegularPackage ?? CorePackage; + public ContentPackage? ContentPackage + => (ContentPackage?)RegularPackage ?? CorePackage; public string GetPackageStr() @@ -58,21 +56,44 @@ namespace Barotrauma.Networking public delegate void DisconnectMessageCallback(string message); public delegate void PasswordCallback(int salt, int retries); public delegate void InitializationCompleteCallback(); + + [Obsolete("TODO: delete in nr3-layer-1-2-cleanup")] + public readonly struct Callbacks + { + public readonly MessageCallback OnMessageReceived; + public readonly DisconnectCallback OnDisconnect; + public readonly DisconnectMessageCallback OnDisconnectMessageReceived; + public readonly PasswordCallback OnRequestPassword; + public readonly InitializationCompleteCallback OnInitializationComplete; + + public Callbacks(MessageCallback onMessageReceived, DisconnectCallback onDisconnect, DisconnectMessageCallback onDisconnectMessageReceived, PasswordCallback onRequestPassword, InitializationCompleteCallback onInitializationComplete) + { + OnMessageReceived = onMessageReceived; + OnDisconnect = onDisconnect; + OnDisconnectMessageReceived = onDisconnectMessageReceived; + OnRequestPassword = onRequestPassword; + OnInitializationComplete = onInitializationComplete; + } + } + + protected readonly Callbacks callbacks; + + public readonly Endpoint ServerEndpoint; + public NetworkConnection? ServerConnection { get; protected set; } + + protected readonly bool isOwner; + protected readonly Option ownerKey; + + public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) + { + ServerEndpoint = serverEndpoint; + this.callbacks = callbacks; + this.ownerKey = ownerKey; + isOwner = ownerKey.IsSome(); + } - public MessageCallback OnMessageReceived; - public DisconnectCallback OnDisconnect; - public DisconnectMessageCallback OnDisconnectMessageReceived; - public PasswordCallback OnRequestPassword; - public InitializationCompleteCallback OnInitializationComplete; - - public string Name; - - public string Version { get; protected set; } - - public NetworkConnection ServerConnection { get; protected set; } - - public abstract void Start(object endPoint, int ownerKey); - public abstract void Close(string msg = null, bool disableReconnect = false); + public abstract void Start(); + public abstract void Close(string? msg = null, bool disableReconnect = false); public abstract void Update(float deltaTime); public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void SendPassword(string password); @@ -80,10 +101,9 @@ namespace Barotrauma.Networking protected abstract void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg); protected ConnectionInitialization initializationStep; - protected bool contentPackageOrderReceived; - protected int ownerKey = 0; + public bool ContentPackageOrderReceived { get; protected set; } protected int passwordSalt; - protected Steamworks.AuthTicket steamAuthTicket; + protected Steamworks.AuthTicket? steamAuthTicket; protected void ReadConnectionInitializationStep(IReadMessage inc) { ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); @@ -97,9 +117,9 @@ namespace Barotrauma.Networking outMsg = new WriteOnlyMessage(); outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); - outMsg.Write(Name); - outMsg.Write(ownerKey); - outMsg.Write(SteamManager.GetSteamID()); + outMsg.Write(GameMain.Client.Name); + outMsg.Write(ownerKey.Fallback(0)); + outMsg.Write(SteamManager.GetSteamId().Select(steamId => steamId.Value).Fallback(0)); if (steamAuthTicket == null) { outMsg.Write((UInt16)0); @@ -122,8 +142,6 @@ namespace Barotrauma.Networking 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++) @@ -139,9 +157,16 @@ namespace Barotrauma.Networking serverPackages.Add(pkg); } - if (!contentPackageOrderReceived) + if (!ContentPackageOrderReceived) { ServerContentPackages = serverPackages.ToImmutableArray(); + if (serverPackages.Count == 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(DeliveryMethod.Reliable, outMsg); } break; @@ -158,7 +183,7 @@ namespace Barotrauma.Networking { retries = inc.ReadInt32(); } - OnRequestPassword?.Invoke(passwordSalt, retries); + callbacks.OnRequestPassword.Invoke(passwordSalt, retries); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index ed3738964..80e5a7f65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; +using Barotrauma.Steam; using Lidgren.Network; -using Barotrauma.Steam; -using System.Linq; +using System; +using System.Collections.Generic; +using System.Text; namespace Barotrauma.Networking { @@ -16,39 +14,42 @@ namespace Barotrauma.Networking 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) + public override void Start() { if (isActive) { return; } - this.ownerKey = ownerKey; - - contentPackageOrderReceived = false; + 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); netClient = new NetClient(netPeerConfiguration); if (SteamManager.IsInitialized) { steamAuthTicket = SteamManager.GetAuthSessionTicket(); - //TODO: wait for GetAuthSessionTicketResponse_t - if (steamAuthTicket == null) { throw new Exception("GetAuthSessionTicket returned null"); @@ -59,7 +60,7 @@ namespace Barotrauma.Networking initializationStep = ConnectionInitialization.SteamTicketAndVersion; - if (!(endPoint is IPEndPoint ipEndPoint)) + if (!(ServerEndpoint is LidgrenEndpoint lidgrenEndpoint)) { throw new InvalidCastException("endPoint is not IPEndPoint"); } @@ -69,7 +70,10 @@ namespace Barotrauma.Networking } netClient.Start(); - ServerConnection = new LidgrenConnection("Server", netClient.Connect(ipEndPoint), 0) + + var netConnection = netClient.Connect(lidgrenEndpoint.NetEndpoint); + + ServerConnection = new LidgrenConnection(netConnection) { Status = NetworkConnectionStatus.Connected }; @@ -81,7 +85,7 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ownerKey != 0 && (ChildServerRelay.Process?.HasExited ?? true)) + if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) { Close(); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); @@ -97,7 +101,7 @@ namespace Barotrauma.Networking foreach (NetIncomingMessage inc in incomingLidgrenMessages) { - if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } + if (!inc.SenderConnection.RemoteEndPoint.Equals(lidgrenEndpoint.NetEndpoint)) { continue; } switch (inc.MessageType) { @@ -125,12 +129,12 @@ namespace Barotrauma.Networking { 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); + callbacks.OnMessageReceived.Invoke(msg); } } @@ -144,7 +148,7 @@ namespace Barotrauma.Networking case NetConnectionStatus.Disconnected: string disconnectMsg = inc.ReadString(); Close(disconnectMsg); - OnDisconnectMessageReceived?.Invoke(disconnectMsg); + callbacks.OnDisconnectMessageReceived.Invoke(disconnectMsg); break; } } @@ -176,7 +180,7 @@ namespace Barotrauma.Networking netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; - OnDisconnect?.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(disableReconnect); } public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 6e097aabf..c4c30ce12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -1,17 +1,16 @@ -using System; +using Barotrauma.Steam; +using System; using System.Collections.Generic; -using System.Text; using System.Linq; -using Barotrauma.Steam; +using System.Text; using System.Threading; -using Barotrauma.Items.Components; namespace Barotrauma.Networking { class SteamP2PClientPeer : ClientPeer { private bool isActive; - private UInt64 hostSteamId; + private readonly SteamId hostSteamId; private double timeout; private double heartbeatTimer; private double connectionStatusTimer; @@ -21,18 +20,23 @@ namespace Barotrauma.Networking private List incomingInitializationMessages; private List incomingDataMessages; - 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; + ContentPackageOrderReceived = false; steamAuthTicket = SteamManager.GetAuthSessionTicket(); //TODO: wait for GetAuthSessionTicketResponse_t @@ -42,21 +46,14 @@ 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(); @@ -66,7 +63,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); outMsg.Write((byte)ConnectionInitialization.ConnectionStarted); - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); sentBytes += outMsg.LengthBytes; initializationStep = ConnectionInitialization.SteamTicketAndVersion; @@ -81,7 +78,7 @@ namespace Barotrauma.Networking private void OnIncomingConnection(Steamworks.SteamId steamId) { if (!isActive) { return; } - if (steamId == hostSteamId) + if (steamId == hostSteamId.Value) { Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); } @@ -90,23 +87,23 @@ namespace Barotrauma.Networking initializationStep != ConnectionInitialization.Success) { DebugConsole.ThrowError($"Connection from incorrect SteamID was rejected: "+ - $"expected {SteamManager.SteamIDUInt64ToString(hostSteamId)}," + - $"got {SteamManager.SteamIDUInt64ToString(steamId)}"); + $"expected {hostSteamId}," + + $"got {new SteamId(steamId)}"); } } private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error) { if (!isActive) { return; } - if (steamId != hostSteamId) { return; } + if (steamId != hostSteamId.Value) { return; } Close($"SteamP2P connection failed: {error}"); - OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); + callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); } private void OnP2PData(ulong steamId, byte[] data, int dataLength) { if (!isActive) { return; } - if (steamId != hostSteamId) { return; } + if (steamId != hostSteamId.Value) { return; } timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame : @@ -138,7 +135,7 @@ namespace Barotrauma.Networking IReadMessage inc = new ReadOnlyMessage(data, false, 1, dataLength - 1, ServerConnection); string msg = inc.ReadString(); Close(msg); - OnDisconnectMessageReceived?.Invoke(msg); + callbacks.OnDisconnectMessageReceived.Invoke(msg); } else { @@ -166,18 +163,18 @@ namespace Barotrauma.Networking connectionStatusTimer -= deltaTime; if (connectionStatusTimer <= 0.0) { - var state = Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId); + var state = Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value); if (state == null) { Close("SteamP2P connection could not be established"); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PError.ToString()); + callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PError.ToString()); } else { if (state?.P2PSessionError != Steamworks.P2PSessionError.None) { Close($"SteamP2P error code: {state?.P2PSessionError}"); - OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state?.P2PSessionError}"); + callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state?.P2PSessionError}"); } } connectionStatusTimer = 1.0f; @@ -204,7 +201,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)DeliveryMethod.Unreliable); outMsg.Write((byte)PacketHeader.IsHeartbeatMessage); - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Unreliable); + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Unreliable); sentBytes += outMsg.LengthBytes; heartbeatTimer = 5.0; @@ -213,7 +210,7 @@ namespace Barotrauma.Networking if (timeout < 0.0) { Close("Timed out"); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); + callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); return; } @@ -221,7 +218,22 @@ namespace Barotrauma.Networking { if (incomingDataMessages.Count > 0) { - OnInitializationComplete?.Invoke(); + var incomingMessage = incomingDataMessages.First(); + byte incomingHeader = incomingMessage.LengthBytes > 0 ? incomingMessage.PeekByte() : (byte)0; + if (ContentPackageOrderReceived) + { +#warning: TODO: do not allow completing initialization until content package order has been received? + string errorMsg = $"Error during connection initialization: completed initialization before receiving content package order. Incoming header: {incomingHeader}"; + GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:ContentPackageOrderNotReceived", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); + } + if (ServerContentPackages.Length == 0) + { + string errorMsg = $"Error during connection initialization: list of content packages enabled on the server was empty when completing initialization. Incoming header: {incomingHeader}"; + GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); + } + callbacks.OnInitializationComplete.Invoke(); initializationStep = ConnectionInitialization.Success; } else @@ -237,7 +249,7 @@ namespace Barotrauma.Networking { foreach (IReadMessage inc in incomingDataMessages) { - OnMessageReceived?.Invoke(inc); + callbacks.OnMessageReceived.Invoke(inc); } } @@ -303,7 +315,7 @@ namespace Barotrauma.Networking private void Send(byte[] buf, int length, Steamworks.P2PSend sendType) { - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, buf, length + 4, 0, sendType); sentBytes += length + 4; if (!successSend) { @@ -311,7 +323,7 @@ namespace Barotrauma.Networking { DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; - successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, buf, length + 4, 0, sendType); sentBytes += length + 4; } if (!successSend) @@ -335,7 +347,7 @@ namespace Barotrauma.Networking outMsg.Write(saltedPw, 0, saltedPw.Length); heartbeatTimer = 5.0; - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); sentBytes += outMsg.LengthBytes; } @@ -354,7 +366,7 @@ namespace Barotrauma.Networking try { - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); sentBytes += outMsg.LengthBytes; } catch (Exception e) @@ -365,12 +377,11 @@ namespace Barotrauma.Networking Thread.Sleep(100); Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId); + Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value); steamAuthTicket?.Cancel(); steamAuthTicket = null; - hostSteamId = 0; - OnDisconnect?.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(disableReconnect); } protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) @@ -394,7 +405,7 @@ namespace Barotrauma.Networking msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); heartbeatTimer = 5.0; - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, msgToSend.Buffer, msgToSend.LengthBytes, 0, sendType); + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msgToSend.Buffer, msgToSend.LengthBytes, 0, sendType); sentBytes += msg.LengthBytes; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 2f5c2f480..0f6641332 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -1,7 +1,7 @@ -using Barotrauma.Steam; +using Barotrauma.Extensions; +using Barotrauma.Steam; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; namespace Barotrauma.Networking @@ -10,20 +10,20 @@ namespace Barotrauma.Networking { private bool isActive; - private readonly UInt64 selfSteamID; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey); + private readonly SteamId selfSteamID; + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - private UInt64 ReadSteamId(IReadMessage inc) - => inc.ReadUInt64() ^ ownerKey64; - private void WriteSteamId(IWriteMessage msg, UInt64 val) - => msg.Write(val ^ ownerKey64); + private SteamId ReadSteamId(IReadMessage inc) + => new SteamId(inc.ReadUInt64() ^ ownerKey64); + private void WriteSteamId(IWriteMessage msg, SteamId val) + => msg.Write(val.Value ^ ownerKey64); private long sentBytes, receivedBytes; class RemotePeer { - public UInt64 SteamID; - public UInt64 OwnerSteamID; + public SteamId SteamId; + public Option OwnerSteamId; public double? DisconnectTime; public bool Authenticating; public bool Authenticated; @@ -33,12 +33,12 @@ namespace Barotrauma.Networking public DeliveryMethod DeliveryMethod; public IWriteMessage Message; } - public List UnauthedMessages; + public readonly List UnauthedMessages; - public RemotePeer(UInt64 steamId) + public RemotePeer(SteamId steamId) { - SteamID = steamId; - OwnerSteamID = 0; + SteamId = steamId; + OwnerSteamId = Option.None(); DisconnectTime = null; Authenticating = false; Authenticated = false; @@ -49,27 +49,27 @@ namespace Barotrauma.Networking } List remotePeers; - public SteamP2POwnerPeer(string name) + 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,10 +82,10 @@ 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); + DebugConsole.Log(steamId + " validation: " + status + ", " + (remotePeer != null)); if (remotePeer == null) { return; } @@ -100,7 +100,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { - remotePeer.OwnerSteamID = ownerID; + remotePeer.OwnerSteamId = Option.Some(new SteamId(ownerId)); remotePeer.Authenticated = true; remotePeer.Authenticating = false; foreach (var msg in remotePeer.UnauthedMessages) @@ -111,7 +111,7 @@ namespace Barotrauma.Networking //known now int prevBitPosition = msg.Message.BitPosition; msg.Message.BitPosition = sizeof(ulong) * 8; - WriteSteamId(msg.Message, ownerID); + WriteSteamId(msg.Message, new SteamId(ownerId)); msg.Message.BitPosition = prevBitPosition; byte[] msgToSend = (byte[])msg.Message.Buffer.Clone(); Array.Resize(ref msgToSend, msg.Message.LengthBytes); @@ -130,34 +130,33 @@ 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, byte[] data, int dataLength, int _) { 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; } + if (remotePeer.DisconnectTime != null) { return; } IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, steamId); - WriteSteamId(outMsg, remotePeer.OwnerSteamID); + var steamUserId = new SteamId(steamId); + WriteSteamId(outMsg, steamUserId); + WriteSteamId(outMsg, remotePeer.OwnerSteamId.Fallback(steamUserId)); outMsg.Write(data, 1, dataLength - 1); DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; PacketHeader packetHeader = (PacketHeader)data[1]; - if (!remotePeer.Authenticated & !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) + if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) { remotePeer.DisconnectTime = null; @@ -240,7 +239,7 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - UInt64 recipientSteamId = ReadSteamId(inc); + SteamId recipientSteamId = ReadSteamId(inc); DeliveryMethod deliveryMethod = (DeliveryMethod)inc.ReadByte(); int p2pDataStart = inc.BytePosition; @@ -255,7 +254,7 @@ namespace Barotrauma.Networking return; } - RemotePeer peer = remotePeers.Find(p => p.SteamID == recipientSteamId); + RemotePeer peer = remotePeers.Find(p => p.SteamId == recipientSteamId); if (peer == null) { return; } @@ -314,7 +313,7 @@ namespace Barotrauma.Networking sendType = Steamworks.P2PSend.Reliable; } - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId.Value, p2pData, p2pData.Length, 0, sendType); sentBytes += p2pData.Length; if (!successSend) @@ -323,7 +322,7 @@ namespace Barotrauma.Networking { DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; - successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId.Value, p2pData, p2pData.Length, 0, sendType); sentBytes += p2pData.Length; } if (!successSend) @@ -354,7 +353,7 @@ namespace Barotrauma.Networking WriteSteamId(outMsg, selfSteamID); WriteSteamId(outMsg, selfSteamID); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); - outMsg.Write(Name); + outMsg.Write(GameMain.Client.Name); byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); Array.Resize(ref msgToSend, outMsg.LengthBytes); @@ -365,12 +364,12 @@ namespace Barotrauma.Networking { if (initializationStep != ConnectionInitialization.Success) { - OnInitializationComplete?.Invoke(); + callbacks.OnInitializationComplete.Invoke(); initializationStep = ConnectionInitialization.Success; } UInt16 length = inc.ReadUInt16(); IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, ServerConnection); - OnMessageReceived?.Invoke(msg); + callbacks.OnMessageReceived.Invoke(msg); return; } @@ -390,7 +389,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)(PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage)); outMsg.Write(msg); - Steamworks.SteamNetworking.SendP2PPacket(peer.SteamID, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); sentBytes += outMsg.LengthBytes; } else @@ -401,7 +400,7 @@ namespace Barotrauma.Networking private void ClosePeerSession(RemotePeer peer) { - Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamID); + Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamId.Value); remotePeers.Remove(peer); } @@ -430,7 +429,7 @@ namespace Barotrauma.Networking ChildServerRelay.ClosePipes(); - OnDisconnect?.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(disableReconnect); SteamManager.LeaveLobby(); Steamworks.SteamNetworking.ResetActions(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 797343808..acfa28618 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading.Tasks; using System.Xml.Linq; @@ -11,13 +10,14 @@ namespace Barotrauma.Networking { class ServerInfo { - public string IP; - public string Port; - public string QueryPort; - - public Steamworks.Data.NetPingLocation? PingLocation; + public Endpoint Endpoint; + + #region TODO: genericize + public int QueryPort; public UInt64 LobbyID; - public UInt64 OwnerID; + public Steamworks.Data.NetPingLocation? PingLocation; + #endregion + public bool OwnerVerified; private string serverName; @@ -42,7 +42,7 @@ namespace Barotrauma.Networking //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; + // TODO: death to Nullable!!!! public SelectionMode? ModeSelectionMode; public SelectionMode? SubSelectionMode; public bool? AllowSpectating; @@ -140,7 +140,7 @@ namespace Barotrauma.Networking } var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), + Endpoint.ServerTypeString, textAlignment: Alignment.TopLeft) { CanBeFocused = false @@ -248,20 +248,6 @@ namespace Barotrauma.Networking 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); @@ -350,34 +336,27 @@ namespace Barotrauma.Networking public static ServerInfo FromXElement(XElement element) { - ServerInfo info = new ServerInfo() + 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 null; } + + 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) + Endpoint = endpoint, + QueryPort = element.GetAttributeInt("QueryPort", 0), + GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty), + GameVersion = element.GetAttributeString("GameVersion", ""), + MaxPlayers = Math.Min(element.GetAttributeInt("MaxPlayers", 0), NetConfig.MaxPlayers), + HasPassword = element.GetAttributeBool("HasPassword", false), + RespondedToSteamQuery = null }; - 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; } @@ -385,8 +364,6 @@ namespace Barotrauma.Networking 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; } @@ -394,9 +371,9 @@ namespace Barotrauma.Networking { if (!SteamManager.IsInitialized) { return; } - if (int.TryParse(QueryPort, out int parsedPort) && IPAddress.TryParse(IP, out IPAddress parsedIP)) + if (QueryPort != 0 && Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ipAddress } }) { - if (MatchmakingPingResponse?.QueryActive ?? false) + if (MatchmakingPingResponse is { QueryActive: true }) { MatchmakingPingResponse.Cancel(); } @@ -433,14 +410,11 @@ namespace Barotrauma.Networking RespondedToSteamQuery = false; }); - MatchmakingPingResponse.HQueryPing(parsedIP, parsedPort); + MatchmakingPingResponse.HQueryPing(ipAddress, QueryPort); } - else if (OwnerID != 0) + else if (Endpoint is SteamP2PEndpoint { SteamId: var ownerId }) { - if (SteamFriend == null) - { - SteamFriend = new Steamworks.Friend(OwnerID); - } + SteamFriend ??= new Steamworks.Friend(ownerId.Value); if (LobbyID == 0) { TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(), @@ -474,20 +448,15 @@ namespace Barotrauma.Networking 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; } + + if (!SteamId.Parse(lobby.GetData("lobbyowner")).TryUnwrap(out var ownerId)) { return; } + if (!(Endpoint is SteamP2PEndpoint { SteamId: var id }) || id != 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; @@ -496,7 +465,7 @@ namespace Barotrauma.Networking public XElement ToXElement() { - if (OwnerID == 0 && string.IsNullOrEmpty(Port)) + if (Endpoint is null) { return null; //can't save this one since it's not set up correctly } @@ -505,22 +474,12 @@ namespace Barotrauma.Networking 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("Endpoint", Endpoint.ToString()); 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()); } @@ -540,14 +499,19 @@ namespace Barotrauma.Networking 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); + other.Endpoint == Endpoint && + (other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0); } + /// + /// This class is trash, so punish its use by making it horribly inefficient in hashsets + /// Doing anything else here would make it cause even more bugs + /// + public override int GetHashCode() => 0; + public bool MatchesByEndpoint(ServerInfo other) { - return OwnerID == other.OwnerID && (OwnerID != 0 ? true : (IP == other.IP && Port == other.Port)); + return other.Endpoint == Endpoint; } } } 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..26cc5098c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -121,7 +121,6 @@ namespace Barotrauma.Networking ReadMonsterEnabled(incMsg); BanList.ClientAdminRead(incMsg); - Whitelist.ClientAdminRead(incMsg); } public void ClientRead(IReadMessage incMsg) @@ -225,7 +224,6 @@ namespace Barotrauma.Networking outMsg.Write(changedMonsterSettings); outMsg.WritePadBits(); if (changedMonsterSettings) WriteMonsterEnabled(outMsg, tempMonsterEnabled); BanList.ClientAdminWrite(outMsg); - Whitelist.ClientAdminWrite(outMsg); } if (dataToSend.HasFlag(NetFlags.HiddenSubs)) @@ -273,8 +271,7 @@ namespace Barotrauma.Networking General, Rounds, Antigriefing, - Banlist, - Whitelist + Banlist } private NetPropertyData GetPropertyData(string name) @@ -949,13 +946,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index db7a406c7..617e7d40f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -53,7 +53,7 @@ namespace Barotrauma.Networking { get { - return GameMain.Client?.ID ?? 0; + return GameMain.Client?.SessionId ?? 0; } protected set { @@ -82,7 +82,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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 90382e7c9..38c935426 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -54,7 +55,7 @@ namespace Barotrauma voteCountMax[voteType] = value; } - public void UpdateVoteTexts(List clients, VoteType voteType) + public void UpdateVoteTexts(IEnumerable clients, VoteType voteType) { switch (voteType) { @@ -92,7 +93,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; } @@ -136,7 +137,7 @@ namespace Barotrauma case VoteType.Kick: if (!(data is Client votedClient)) { return; } - msg.Write(votedClient.ID); + msg.Write(votedClient.SessionId); break; case VoteType.StartRound: if (!(data is bool)) { return; } @@ -233,21 +234,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 +260,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 +286,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 +345,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/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 303b9d94f..fa017d9c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -390,7 +390,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; } @@ -489,7 +489,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.Disconnect(); + GameMain.Client.Quit(); GameMain.Client = null; } @@ -834,9 +834,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; @@ -865,8 +865,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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 537124f37..599aa2c60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -68,12 +68,28 @@ 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()) @@ -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)); + ContentPackageManager.EnabledPackages.SetRegular(regularPackages); } GameMain.NetLobbyScreen.Select(); return; @@ -153,7 +172,7 @@ namespace Barotrauma buttonContainerSpacing(0.2f); button(TextManager.Get("No"), () => { - GameMain.Client?.Disconnect(); + GameMain.Client?.Quit(); GameMain.MainMenuScreen.Select(); }); buttonContainerSpacing(0.1f); @@ -173,10 +192,10 @@ 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(); + new ConnectCommand( + serverName: GameMain.Client.ServerName, + endpoint: GameMain.Client.ClientPeer.ServerEndpoint)); + GameMain.Client.Quit(); } GameMain.MainMenuScreen.Select(); }, width: 0.7f); @@ -275,19 +294,22 @@ namespace Barotrauma ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) ?.CorePackage ?? throw new Exception($"Failed to find core package to enable"); - RegularPackage[] regularPackages + List 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(); + .ToList(); 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)); + ContentPackageManager.EnabledPackages.BackUp(); ContentPackageManager.EnabledPackages.SetCore(corePackage); ContentPackageManager.EnabledPackages.SetRegular(regularPackages); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 4bf99f6d8..7321d64d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1529,14 +1529,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; @@ -2160,15 +2159,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 +2180,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 +2234,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 +2279,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 +2508,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 +2518,7 @@ 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), + Enabled = !selectedClient.HasKickVoteFromSessionId(GameMain.Client.SessionId), OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; }, UserData = selectedClient }; @@ -2560,7 +2550,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 +2560,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 +2618,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 +2663,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 49bed19f7..984cb5cbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -53,21 +53,19 @@ namespace Barotrauma private class FriendInfo { - public UInt64 SteamID; + 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 Option ConnectCommand = Option.None(); public bool InServer { get { - return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); + return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && ConnectCommand.IsSome(); } } } @@ -81,7 +79,7 @@ namespace Barotrauma private List favoriteServers; private List recentServers; - private readonly Dictionary activePings = new Dictionary(); + private readonly Dictionary activePings = new Dictionary(); private enum ServerListTab { @@ -153,7 +151,6 @@ namespace Barotrauma private GUITickBox filterIncompatible; private GUITickBox filterFull; private GUITickBox filterEmpty; - private GUITickBox filterWhitelisted; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; @@ -405,7 +402,6 @@ namespace Barotrauma filterIncompatible = addTickBox("FilterIncompatibleServers".ToIdentifier()); filterFull = addTickBox("FilterFullServers".ToIdentifier()); filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); - filterWhitelisted = addTickBox("FilterWhitelistedServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); // Filter Tags @@ -608,14 +604,14 @@ namespace Barotrauma { 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) + if (selectedServer.LobbyID != 0) { Steam.SteamManager.JoinLobby(selectedServer.LobbyID, true); } + else if (selectedServer.Endpoint != null) + { + JoinServer(selectedServer.Endpoint, selectedServer.ServerName); + } else { new GUIMessageBox("", TextManager.Get("ServerOffline")); @@ -753,20 +749,11 @@ namespace Barotrauma doc.SaveSafe(file); } - public ServerInfo UpdateServerInfoWithServerSettings(NetworkConnection endpoint, ServerSettings serverSettings) + public ServerInfo UpdateServerInfoWithServerSettings(NetworkConnection connection, 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; + serverInfo.Endpoint == connection.Endpoint)?.UserData as ServerInfo; if (info == null) { isInfoNew = true; @@ -775,17 +762,14 @@ namespace Barotrauma info.ServerName = serverSettings.ServerName; info.ServerMessage = serverSettings.ServerMessageText; - info.OwnerID = steamId; + info.Endpoint = connection.Endpoint; 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; @@ -807,10 +791,9 @@ namespace Barotrauma public void AddToRecentServers(ServerInfo info) { - if (!string.IsNullOrEmpty(info.IP)) + if (info.Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ip } } && IPAddress.IsLoopback(ip)) { - //don't add localhost to recent servers - if (IPAddress.TryParse(info.IP, out IPAddress ip) && IPAddress.IsLoopback(ip)) { return; } + return; } info.Recent = true; @@ -870,8 +853,7 @@ namespace Barotrauma private void SortList(string sortBy, bool toggle) { - GUIButton button = labelHolder.GetChildByUserData(sortBy) as GUIButton; - if (button == null) { return; } + if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } sortedBy = sortBy; @@ -985,7 +967,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.Disconnect(); + GameMain.Client.Quit(); GameMain.Client = null; } @@ -1060,14 +1042,13 @@ namespace Barotrauma (!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.All && (serverInfo.LobbyID != 0 || serverInfo.Endpoint != null)) || (selectedTab == ServerListTab.Recent && serverInfo.Recent) || (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); } @@ -1105,7 +1086,7 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } - private Queue pendingQueries = new Queue(); + private readonly Queue pendingQueries = new Queue(); int activeQueries = 0; private void QueueInfoQuery(ServerInfo info) { @@ -1152,46 +1133,22 @@ namespace Barotrauma okButton.Enabled = false; okButton.OnClicked = (btn, userdata) => { - JoinServer(endpointBox.Text, ""); + if (!(Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))) { return false; } + JoinServer(endpoint, ""); msgBox.Close(); - return true; + return false; }; 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; + if (!(Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))) { return false; } - 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(), + Endpoint = endpoint, GameVersion = GameMain.Version.ToString(), PlayStyle = null }; @@ -1231,37 +1188,25 @@ namespace Barotrauma private bool JoinFriend(GUIButton button, object userdata) { - FriendInfo info = userdata as FriendInfo; + if (!(userdata is FriendInfo { InServer: true } info)) { return false; } - 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; - } - } + GameMain.Instance.ConnectCommand = info.ConnectCommand; return false; } private bool OpenFriendPopup(GUIButton button, object userdata) { - FriendInfo info = userdata as FriendInfo; + if (!(userdata is FriendInfo { InServer: true } info)) { return false; } - if (info.InServer) + if (info.InServer + && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } + && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { - int framePadding = 5; + 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), info.ConnectName ?? "[Unnamed]"); + 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")) @@ -1349,7 +1294,7 @@ namespace Barotrauma for (int i = friendsList.Count - 1; i >= 0; i--) { var friend = friendsList[i]; - if (!friends.Any(g => g.Id == friend.SteamID && g.IsOnline)) + if (!friends.Any(g => g.Id == friend.SteamId && g.IsOnline)) { friend.Sprite?.Remove(); friendsList.RemoveAt(i); @@ -1360,12 +1305,12 @@ namespace Barotrauma { if (!friend.IsOnline) { continue; } - FriendInfo info = friendsList.Find(f => f.SteamID == friend.Id); + FriendInfo info = friendsList.Find(f => f.SteamId == friend.Id); if (info == null) { info = new FriendInfo() { - SteamID = friend.Id + SteamId = friend.Id }; friendsList.Insert(0, info); } @@ -1425,9 +1370,7 @@ namespace Barotrauma info.Name = friend.Name; - info.ConnectName = null; - info.ConnectEndpoint = null; - info.ConnectLobby = 0; + info.ConnectCommand = Option.None(); info.PlayingThisGame = friend.IsPlayingThisGame; info.PlayingAnotherGame = friend.GameInfo.HasValue; @@ -1439,7 +1382,7 @@ namespace Barotrauma try { - ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand), out info.ConnectName, out info.ConnectEndpoint, out info.ConnectLobby); + info.ConnectCommand = ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand)); } catch (IndexOutOfRangeException e) { @@ -1448,9 +1391,7 @@ namespace Barotrauma #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; + info.ConnectCommand = Option.None(); } } else @@ -1650,86 +1591,16 @@ namespace Barotrauma 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 GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo) + => serverList.Content.FindChild(d => + d.UserData is ServerInfo info + && (info.LobbyID == 0 || info.LobbyID == serverInfo.LobbyID) + && info.OwnerVerified + && serverInfo.Endpoint == info.Endpoint); + 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))); + var serverFrame = FindFrameMatchingServerInfo(serverInfo); if (serverFrame == null) { @@ -1763,8 +1634,10 @@ namespace Barotrauma 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(); + var childrenToRemove = serverList.Content.FindChildren(c => + c.UserData is ServerInfo info + && !ReferenceEquals(info, serverInfo) + && serverInfo.Endpoint == info.Endpoint).ToList(); foreach (var child in childrenToRemove) { serverList.Content.RemoveChild(child); @@ -1779,11 +1652,7 @@ namespace Barotrauma 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))); + var serverFrame = FindFrameMatchingServerInfo(serverInfo); if (serverFrame == null) return; var serverContent = serverFrame.Children.First() as GUILayoutGroup; @@ -1806,11 +1675,10 @@ namespace Barotrauma }; 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, +#if DEBUG + (serverInfo.Endpoint is SteamP2PEndpoint ? "[STEAMP2P] " : "[LIDGREN] ") + #endif + serverInfo.ServerName, style: "GUIServerListTextBox"); serverName.UserData = serverName.Text; serverName.RectTransform.SizeChanged += () => @@ -1850,7 +1718,7 @@ namespace Barotrauma serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); } - else if (!string.IsNullOrEmpty(serverInfo.IP)) + else if (serverInfo.Endpoint is LidgrenEndpoint lidgrenEndpoint) { try { @@ -1866,7 +1734,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(EstimateLobbyPing(serverInfo, serverPingText), "EstimateLobbyPing"); } - if (serverInfo.LobbyID == 0 && (string.IsNullOrWhiteSpace(serverInfo.IP) || string.IsNullOrWhiteSpace(serverInfo.Port))) + if (serverInfo.LobbyID == 0) { LocalizedString toolTip = TextManager.Get("ServerOffline"); serverContent.Children.ForEach(c => c.ToolTip = toolTip); @@ -1930,7 +1798,7 @@ namespace Barotrauma LocalizedString toolTip = ""; for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { - if (!ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) + if (ContentPackageManager.EnabledPackages.All.None(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) { if (toolTip != "") { toolTip += "\n"; } toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); @@ -1995,7 +1863,7 @@ namespace Barotrauma masterServerResponded = true; } - private bool JoinServer(string endpoint, string serverName) + private bool JoinServer(Endpoint endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { @@ -2013,17 +1881,13 @@ namespace Barotrauma return true; } - private IEnumerable ConnectToServer(string endpoint, string serverName) + private IEnumerable ConnectToServer(Endpoint 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); + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), endpoint, serverName, Option.None()); #if !DEBUG } catch (Exception e) @@ -2035,31 +1899,32 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void GetServerPing(ServerInfo serverInfo, GUITextBlock serverPingText) + private void GetServerPing(ServerInfo serverInfo, GUITextBlock serverPingText) { if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } - + if (!(serverInfo.Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var address } })) { return; } + lock (activePings) { - if (activePings.ContainsKey(serverInfo.IP)) { return; } - activePings.Add(serverInfo.IP, activePings.Any() ? activePings.Values.Max()+1 : 0); + if (activePings.ContainsKey(address)) { return; } + activePings.Add(address, activePings.Any() ? activePings.Values.Max()+1 : 0); } serverInfo.PingChecked = false; serverInfo.Ping = -1; - TaskPool.Add($"PingServerAsync ({serverInfo?.IP ?? "NULL"})", PingServerAsync(serverInfo.IP, 1000), + TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), new Tuple(serverInfo, serverPingText), (rtt, obj) => { - var info = obj.Item1; - var text = obj.Item2; - rtt.TryGetResult(out info.Ping); info.PingChecked = true; + var (info, text) = obj; + if (!rtt.TryGetResult(out info.Ping)) { info.Ping = -1; } + info.PingChecked = true; text.TextColor = GetPingTextColor(info.Ping); text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; lock (activePings) { - activePings.Remove(info.IP); + activePings.Remove(address); } }); } @@ -2070,7 +1935,7 @@ namespace Barotrauma return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public async Task PingServerAsync(string ip, int timeOut) + public async Task PingServerAsync(IPAddress ipAddress, int timeOut) { await Task.Yield(); bool shouldGo = false; @@ -2078,29 +1943,22 @@ namespace Barotrauma { lock (activePings) { - shouldGo = activePings.Count(kvp => kvp.Value < activePings[ip]) < 25; + shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 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) + if (ipAddress != null) { //don't attempt to ping if the address is IPv6 and it's not supported - if (address.AddressFamily != AddressFamily.InterNetworkV6 || Socket.OSSupportsIPv6) + if (ipAddress.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)); + PingReply pingReply = ping.Send(ipAddress, timeOut, buffer, new PingOptions(128, true)); if (pingReply != null) { @@ -2117,9 +1975,9 @@ namespace Barotrauma } catch (Exception ex) { - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ip, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); + 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 (" + ip + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); + DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); #endif } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 11ae9f941..96e66ded3 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; @@ -926,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; @@ -1645,7 +1646,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 @@ -5192,6 +5193,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); @@ -5528,8 +5534,10 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) && - dummyCharacter?.SelectedItem == 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) { @@ -5556,7 +5564,7 @@ namespace Barotrauma { 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); } @@ -5708,7 +5716,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/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index b7e2d7744..3040f674e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -382,10 +382,9 @@ namespace Barotrauma LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.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; } @@ -700,9 +699,12 @@ namespace Barotrauma List prevSelected = MapEntity.SelectedList.ToList(); //reselect the entities that were selected during editing //otherwise multi-editing won't work when we deselect the entities with unapplied changes in the textbox - foreach (var entity in editedEntities) - { - MapEntity.SelectedList.Add(entity); + if (editedEntities.Count > 1) + { + foreach (var entity in editedEntities) + { + MapEntity.SelectedList.Add(entity); + } } if (SetPropertyValue(property, entity, textBox.Text)) { 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 ec631166a..8cfa4a8f7 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; 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/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..4a25ebe63 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); }); }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 1243e39d7..7701c1469 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -92,7 +92,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); @@ -101,7 +103,6 @@ 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("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); @@ -144,9 +145,10 @@ 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)))); } }); } @@ -189,14 +191,16 @@ namespace Barotrauma.Steam { 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; + ServerInfo serverInfo = new ServerInfo + { + ServerName = lobby.GetData("name"), + Endpoint = new SteamP2PEndpoint(SteamId.Parse(lobby.GetData("lobbyowner")).Fallback(default(SteamId))), + LobbyID = lobby.Id, + RespondedToSteamQuery = true + }; 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); @@ -215,8 +219,7 @@ namespace Barotrauma.Steam { ServerName = info.Name, HasPassword = info.Passworded, - IP = info.Address.ToString(), - Port = info.ConnectionPort.ToString(), + Endpoint = new LidgrenEndpoint(info.Address, info.ConnectionPort), PlayerCount = info.Players, MaxPlayers = info.MaxPlayers, RespondedToSteamQuery = responsive @@ -316,7 +319,6 @@ namespace Barotrauma.Steam 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; } @@ -365,7 +367,7 @@ namespace Barotrauma.Steam if (rules.ContainsKey("playercount")) { - if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; + if (int.TryParse(rules["playercount"], out int playerCount)) { serverInfo.PlayerCount = playerCount; } } serverInfo.ContentPackageNames.Clear(); @@ -383,7 +385,6 @@ namespace Barotrauma.Steam 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; } 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/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 351ebb833..0d401f95a 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; } }; @@ -234,7 +234,7 @@ 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(); @@ -582,7 +582,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; } @@ -639,9 +639,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 diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 1a24d958e..c9cbd489d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -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..6d6cc04ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -546,7 +546,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/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/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 04e63b8ad..560289dd2 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 0081ad967..4e6d3e5a8 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index dfa092086..54cc0ce82 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index cf881db47..83cba5533 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index b7101a67b..36a90f48b 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 65f10e4c2..37dd065db 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 Endpoint OwnerClientEndpoint; public string OwnerClientName; public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -423,7 +423,7 @@ namespace Barotrauma 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.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0); break; case CharacterStatusEventData statusEventData: WriteStatus(msg, statusEventData.ForceAfflictionData); @@ -632,6 +632,7 @@ namespace Barotrauma } msg.Write(Enabled); + msg.Write(DisabledByEvent); //character with no characterinfo (e.g. some monster) if (Info == null) @@ -644,7 +645,7 @@ namespace Barotrauma if (ownerClient != null) { msg.Write(true); - msg.Write(ownerClient.ID); + msg.Write(ownerClient.SessionId); } else if (GameMain.Server.Character == this) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 58cb3e0ee..6c3a29f56 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; +using Barotrauma.Steam; namespace Barotrauma { @@ -299,10 +300,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 (Endpoint.Parse(arg).TryUnwrap(out var endpoint)) + { + client ??= GameMain.Server.ConnectedClients.Find(c => c.EndpointMatches(endpoint)); + } + 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; } @@ -872,7 +879,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); }); @@ -881,7 +888,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); }); @@ -904,10 +911,12 @@ namespace Barotrauma client); }); - AssignOnExecute("banendpoint", (string[] args) => + AssignOnExecute("banaddress", (string[] args) => { if (GameMain.Server == null || args.Length == 0) return; + if (!(Endpoint.Parse(args[0]).TryUnwrap(out var endpoint))) { return; } + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"? (c to cancel)", (reason) => { if (reason == "c" || reason == "C") { return; } @@ -925,16 +934,16 @@ namespace Barotrauma banDuration = parsedBanDuration; } - var clients = GameMain.Server.ConnectedClients.FindAll(c => c.EndpointMatches(args[0])); + var clients = GameMain.Server.ConnectedClients.Where(c => c.EndpointMatches(endpoint)).ToList(); if (clients.Count == 0) { - GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", args[0], reason, banDuration); + GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", endpoint, reason, banDuration); } else { foreach (Client cl in clients) { - GameMain.Server.BanClient(cl, reason, false, banDuration); + GameMain.Server.BanClient(cl, reason, banDuration); } } }); @@ -1036,7 +1045,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); })); @@ -1045,7 +1055,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); }); @@ -1496,11 +1506,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 (!(Endpoint.Parse(args[0]).TryUnwrap(out var endpoint))) { return; } + var clients = GameMain.Server.ConnectedClients.Where(c => c.EndpointMatches(endpoint)).ToList(); TimeSpan? duration = null; if (args.Length > 1) { @@ -1519,13 +1530,13 @@ namespace Barotrauma if (clients.Count == 0) { - GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", args[0], reason, duration); + GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", endpoint, reason, duration); } else { foreach (Client cl in clients) { - GameMain.Server.BanClient(cl, reason, false, duration); + GameMain.Server.BanClient(cl, reason, duration); } } } @@ -1535,7 +1546,7 @@ namespace Barotrauma { if (GameMain.Server == null || args.Length == 0) return; string clientName = string.Join(" ", args); - GameMain.Server.UnbanPlayer(clientName, ""); + GameMain.Server.UnbanPlayer(clientName); }, () => { @@ -1546,17 +1557,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() }; })); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 42b514127..be7481ad2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -151,9 +151,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(); XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (doc?.Root == null) @@ -169,7 +169,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 @@ -218,12 +218,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": @@ -274,7 +274,7 @@ namespace Barotrauma public void CloseServer() { - Server?.Disconnect(); + Server?.Quit(); ShouldRun = false; Server = null; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 671dedf34..e81939b28 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -15,8 +15,8 @@ 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"); @@ -55,13 +55,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 +89,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) @@ -136,8 +137,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 61caf6e9d..e9d0dfbe0 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() @@ -805,6 +811,10 @@ namespace Barotrauma Wallet personalWallet = GetWallet(sender); personalWallet?.ForceUpdate(); + if (AllowedToManageWallets(sender)) + { + Bank.ForceUpdate(); + } if (purchasedHullRepairs != PurchasedHullRepairs) { @@ -886,7 +896,9 @@ namespace Barotrauma foreach (var item in store.Value.ToList()) { if (map?.CurrentLocation?.Stores == null || !map.CurrentLocation.Stores.ContainsKey(store.Key)) { continue; } - item.Quantity = Math.Min(map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0, item.Quantity); + 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; + item.Quantity = Math.Min(availableQuantity - alreadyPurchasedQuantity, item.Quantity); if (item.Quantity <= 0) { continue; } CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); } @@ -1364,8 +1376,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/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs index 3ac8dd6a9..d7e547b2b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -26,7 +26,7 @@ namespace Barotrauma if (sender != null) { msg.Write(true); - msg.Write(sender.ID); + msg.Write(sender.SessionId); } else { @@ -99,10 +99,10 @@ namespace Barotrauma case ReadyCheckState.Update when readyCheck != null: ReadyStatus status = (ReadyStatus) inc.ReadByte(); - if (!readyCheck.Clients.ContainsKey(client.ID)) { return; } + 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/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 347ef4ed5..bd1572c90 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -59,8 +59,8 @@ namespace Barotrauma.Items.Components msg.Write(timeUntilReady); uint recipeHash = fabricatedItem?.RecipeHash ?? 0; msg.Write(recipeHash); - UInt16 userID = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; - msg.Write(userID); + UInt16 userId = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; + msg.Write(userId); var reachedLimits = fabricationLimits.Where(kvp => kvp.Value <= 0); msg.Write((ushort)reachedLimits.Count()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index c324baf78..3b628dea4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -9,56 +9,16 @@ 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 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()); } } @@ -84,10 +44,10 @@ namespace Barotrauma.Networking 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])) @@ -99,84 +59,57 @@ namespace Barotrauma.Networking } 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)); } } } - public bool IsBanned(IPAddress IP, ulong steamID, ulong ownerSteamID, out string reason) + public void RemoveExpired() { - 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; - } - - 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; + } + + 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; } - public bool IsBanned(ulong steamID, out string reason) + public bool IsBanned(AccountId accountId, 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; + 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, IPAddress ip, string reason, TimeSpan? duration) + 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) { - 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); + var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId); if (existingBan != null) { - if (!duration.HasValue) return; + if (!duration.HasValue) { return; } DebugConsole.Log("Set \"" + name + "\"'s ban duration to " + duration.Value); existingBan.ExpirationTime = DateTime.Now + duration.Value; @@ -187,8 +120,8 @@ namespace Barotrauma.Networking System.Diagnostics.Debug.Assert(!name.Contains(',')); 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 +131,20 @@ 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,22 +162,6 @@ 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); @@ -283,9 +174,9 @@ namespace Barotrauma.Networking foreach (BannedPlayer banned in bannedPlayers) { string line = banned.Name; - line += "," + ((banned.SteamID > 0) ? SteamManager.SteamIDUInt64ToString(banned.SteamID) : banned.EndPoint); + line += "," + (banned.AddressOrAccountId.ToString()); line += "," + (banned.ExpirationTime.HasValue ? banned.ExpirationTime.Value.ToString() : ""); - if (!string.IsNullOrWhiteSpace(banned.Reason)) line += "," + banned.Reason; + if (!string.IsNullOrWhiteSpace(banned.Reason)) { line += "," + banned.Reason; } lines.Add(line); } @@ -324,7 +215,6 @@ namespace Barotrauma.Networking outMsg.Write(bannedPlayer.Name); outMsg.Write(bannedPlayer.UniqueIdentifier); - outMsg.Write(bannedPlayer.IsRangeBan); outMsg.Write(bannedPlayer.ExpirationTime != null); outMsg.WritePadBits(); if (bannedPlayer.ExpirationTime != null) @@ -337,12 +227,19 @@ namespace Barotrauma.Networking if (c.Connection == GameMain.Server.OwnerConnection) { - outMsg.Write(bannedPlayer.EndPoint); - outMsg.Write(bannedPlayer.SteamID); + if (bannedPlayer.AddressOrAccountId.TryGet(out Address endpoint)) + { + outMsg.Write(true); outMsg.WritePadBits(); + outMsg.Write(endpoint.StringRepresentation); + } + else + { + outMsg.Write(false); outMsg.WritePadBits(); + outMsg.Write(((SteamId)bannedPlayer.AddressOrAccountId).StringRepresentation); + } } } } - catch (Exception e) { string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); @@ -355,38 +252,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(); + 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 075a0bd30..d2a56a792 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -212,7 +212,7 @@ namespace Barotrauma.Networking msg.Write(SenderClient != null); if (SenderClient != null) { - msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + msg.Write(SenderClient.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : SenderClient.SessionId.ToString()); } msg.Write(Sender != null && c.InGame); if (Sender != null && c.InGame) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index f385efcde..63c6ad087 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Networking public bool ReadyToStart; - public List JobPreferences; + public List JobPreferences { get; set; } public JobVariant AssignedJob; public float DeleteDisconnectedTimer; @@ -111,7 +111,7 @@ namespace Barotrauma.Networking { 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 @@ -162,7 +162,7 @@ namespace Barotrauma.Networking return true; } - public bool EndpointMatches(string endPoint) + public bool EndpointMatches(Endpoint endPoint) { return Connection.EndpointMatches(endPoint); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 74984923c..ce5282fa4 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; @@ -85,7 +84,7 @@ namespace Barotrauma.Networking } #endif - public override List ConnectedClients + public override IReadOnlyList ConnectedClients { get { @@ -110,10 +109,19 @@ namespace Barotrauma.Networking 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, 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, + int port, + int queryPort, + bool isPublic, + string password, + bool attemptUPnP, + int maxPlayers, + Option ownerKey, + Option ownerSteamId) { name = name.Replace(":", ""); name = name.Replace(";", ""); @@ -132,9 +140,9 @@ namespace Barotrauma.Networking Voting = new Voting(); - ownerKey = ownKey; + this.ownerKey = ownerKey; - ownerSteamId = steamId; + this.ownerSteamId = ownerSteamId; entityEventManager = new ServerEntityEventManager(this); @@ -147,15 +155,15 @@ namespace Barotrauma.Networking try { Log("Starting the server...", ServerLog.MessageType.ServerMessage); - if (!ownerSteamId.HasValue || ownerSteamId.Value == 0) + if (ownerSteamId.TryUnwrap(out var steamId)) { - 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); + Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); + serverPeer = new SteamP2PServerPeer(steamId, ownerKey.Fallback(0), serverSettings); } else { - Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, ownerKey.Value, serverSettings); + 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); } serverPeer.OnInitializationComplete = OnInitializationComplete; @@ -253,17 +261,15 @@ namespace Barotrauma.Networking 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) @@ -299,10 +305,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) { @@ -346,7 +352,7 @@ namespace Barotrauma.Networking if (OwnerConnection != null && ChildServerRelay.HasShutDown) { - Disconnect(); + Quit(); return; } @@ -377,7 +383,7 @@ namespace Barotrauma.Networking character.KillDisconnectedTimer += deltaTime; 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.EndpointMatches(character.OwnerClientEndpoint)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) { @@ -679,7 +685,7 @@ namespace Barotrauma.Networking pingInf.Write((byte)ConnectedClients.Count); ConnectedClients.ForEach(c2 => { - pingInf.Write(c2.ID); + pingInf.Write(c2.SessionId); pingInf.Write(c2.Ping); }); serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable); @@ -788,11 +794,11 @@ namespace Barotrauma.Networking 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; } @@ -1010,14 +1016,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) @@ -1165,6 +1171,12 @@ namespace Barotrauma.Networking 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 (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 { @@ -1189,7 +1201,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.##")); } @@ -1241,7 +1253,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; } } } @@ -1341,7 +1353,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; @@ -1351,7 +1362,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 { @@ -1359,7 +1370,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 { @@ -1368,9 +1379,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(); @@ -1506,7 +1524,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); @@ -1592,7 +1610,7 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); } - outmsg.Write(c.ID); + outmsg.Write(c.SessionId); var subList = GameMain.NetLobbyScreen.GetSubList(); outmsg.Write((UInt16)subList.Count); @@ -1811,15 +1829,15 @@ namespace Barotrauma.Networking { 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 ? 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, @@ -2382,7 +2400,7 @@ namespace Barotrauma.Networking mpCampaign.ClearSavedExperiencePoints(teamClients[i]); } - spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; + spawnedCharacter.OwnerClientEndpoint = teamClients[i].Connection.Endpoint; spawnedCharacter.OwnerClientName = teamClients[i].Name; } @@ -2693,9 +2711,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; } c.PreferredJob = newJob; c.PreferredTeam = newTeam; @@ -2716,7 +2740,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; } @@ -2797,7 +2820,7 @@ namespace Barotrauma.Networking DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); } - 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) || @@ -2809,10 +2832,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; } @@ -2827,45 +2850,32 @@ namespace Barotrauma.Networking string targetMsg = DisconnectReason.Banned.ToString(); DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); - if (client.Connection is LidgrenConnection lidgrenConn && (client.SteamID == 0 || range)) + serverSettings.BanList.BanPlayer(client.Name, client.Connection.Endpoint, reason, duration); + 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) + foreach (var relatedId in client.AccountInfo.OtherMatchingIds) { - serverSettings.BanList.BanPlayer(client.Name, client.SteamID, reason, duration); - } - if (client.OwnerSteamID > 0) - { - 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.Endpoint, 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}"; @@ -2876,16 +2886,17 @@ 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 override void UnbanPlayer(Endpoint endpoint) + { + serverSettings.BanList.UnbanPlayer(endpoint); } public void DisconnectClient(NetworkConnection senderConnection, string msg = "", string targetmsg = "") @@ -2906,7 +2917,7 @@ namespace Barotrauma.Networking { if (client == null) return; - if (gameStarted && client.Character != null) + if (client.Character != null) { client.Character.ClientDisconnected = true; client.Character.ClearInputs(); @@ -2925,7 +2936,7 @@ namespace Barotrauma.Networking 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) @@ -3363,26 +3374,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( 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( client.Name, - client.Connection.EndPointString, + client.Connection.Endpoint.Address, client.Permissions, client.PermittedConsoleCommands)); } @@ -3504,7 +3515,7 @@ namespace Barotrauma.Networking if (client.Character != null) { client.Character.IsRemotePlayer = false; - client.Character.OwnerClientEndPoint = null; + client.Character.OwnerClientEndpoint = null; client.Character.OwnerClientName = null; } @@ -3531,7 +3542,7 @@ namespace Barotrauma.Networking newCharacter.Info.Character = newCharacter; } - newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; + newCharacter.OwnerClientEndpoint = client.Connection.Endpoint; newCharacter.OwnerClientName = client.Name; newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; @@ -3920,17 +3931,7 @@ 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 override void Quit() { if (started) { @@ -3959,12 +3960,11 @@ namespace Barotrauma.Networking } } - partial class PreviousPlayer + class PreviousPlayer { public string Name; - public string EndPoint; - public UInt64 SteamID; - public UInt64 OwnerSteamID; + public Endpoint Endpoint; + public AccountInfo AccountInfo; public float Karma; public int KarmaKickCount; public readonly List KickVoters = new List(); @@ -3972,15 +3972,14 @@ namespace Barotrauma.Networking public PreviousPlayer(Client c) { Name = c.Name; - EndPoint = c.Connection?.EndPointString ?? ""; - SteamID = c.SteamID; - OwnerSteamID = c.OwnerSteamID; + Endpoint = c.Connection.Endpoint; + 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.EndpointMatches(Endpoint); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index 1aad3a99d..0cb30a078 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 { @@ -13,7 +14,7 @@ namespace Barotrauma.Networking msg.Write(SenderClient != null); if (SenderClient != null) { - msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + msg.Write(SenderClient.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : SenderClient.SessionId.ToString()); } msg.Write(Sender != null && c.InGame); if (Sender != null && c.InGame) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 6e52af5c4..4cea49857 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Linq; +using Barotrauma.Steam; using Lidgren.Network; namespace Barotrauma.Networking @@ -13,7 +14,7 @@ namespace Barotrauma.Networking private readonly List incomingLidgrenMessages; - public LidgrenServerPeer(int? ownKey, ServerSettings settings) + public LidgrenServerPeer(Option ownKey, ServerSettings settings) { serverSettings = settings; @@ -184,7 +185,7 @@ namespace Barotrauma.Networking 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); @@ -195,7 +196,7 @@ namespace Barotrauma.Networking if (pendingClient == null) { - pendingClient = new PendingClient(new LidgrenConnection("PENDING", inc.SenderConnection, 0)); + pendingClient = new PendingClient(new LidgrenConnection(inc.SenderConnection)); pendingClients.Add(pendingClient); } @@ -206,7 +207,7 @@ namespace Barotrauma.Networking { 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 == inc.SenderConnection); PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); @@ -231,7 +232,9 @@ namespace Barotrauma.Networking 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); return; @@ -264,7 +267,7 @@ namespace Barotrauma.Networking } else { - disconnectMsg = $"ServerMessage.HasDisconnected~[client]={conn.Name}"; + disconnectMsg = $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == conn).Name}"; Disconnect(conn, disconnectMsg); } } @@ -285,18 +288,18 @@ 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)); + 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 == null) { if (status != Steamworks.AuthResponse.OK) { - LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID) as LidgrenConnection; + LidgrenConnection connection = connectedClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) as LidgrenConnection; if (connection != null) { Disconnect(connection, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); @@ -307,7 +310,9 @@ namespace Barotrauma.Networking LidgrenConnection pendingConnection = pendingClient.Connection as LidgrenConnection; string banReason; - if (serverSettings.BanList.IsBanned(pendingConnection.NetConnection.RemoteEndPoint.Address, steamID, ownerID, out banReason)) + if (serverSettings.BanList.IsBanned(pendingConnection.Endpoint, out banReason) + || serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason) + || serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason)) { RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); return; @@ -315,7 +320,7 @@ namespace Barotrauma.Networking 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; } @@ -333,7 +338,7 @@ namespace Barotrauma.Networking if (!(conn is LidgrenConnection lidgrenConn)) return; if (!connectedClients.Contains(lidgrenConn)) { - DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + lidgrenConn.IPString); + DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + lidgrenConn.Endpoint.StringRepresentation); return; } @@ -368,7 +373,7 @@ namespace Barotrauma.Networking NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); 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.StringRepresentation+": " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow); } } @@ -382,7 +387,7 @@ namespace Barotrauma.Networking lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); OnDisconnect?.Invoke(conn, msg); - Steam.SteamManager.StopAuthSession(conn.SteamID); + if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { Steam.SteamManager.StopAuthSession(steamId); } } lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected"); } @@ -409,25 +414,25 @@ namespace Barotrauma.Networking NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); 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.StringRepresentation + ": " + result.ToString(), 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; } } - protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + protected override void ProcessAuthTicket(string name, Option ownKey, Option steamId, PendingClient pendingClient, byte[] ticket) { - if (pendingClient.SteamID == null) + if (pendingClient.AccountInfo.AccountId.IsNone()) { bool requireSteamAuth = GameSettings.CurrentConfig.RequireSteamAuthentication; #if DEBUG @@ -436,32 +441,42 @@ namespace Barotrauma.Networking //steam auth cannot be done (SteamManager not initialized or no ticket given), //but it's not required either -> let the client join without auth - if ((!Steam.SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) && - !requireSteamAuth) + if ((!SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) + && !requireSteamAuth) { - pendingClient.Connection.Name = name; pendingClient.Name = name; pendingClient.OwnerKey = ownKey; pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; } else { - Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) + if (!steamId.TryUnwrap(out var id)) { if (requireSteamAuth) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: Steam ID not provided"); return; } - else + } + else + { + Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, id); + if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - steamId = 0; - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + if (requireSteamAuth) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); + return; + } + else + { + steamId = Option.None(); + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + } } } - pendingClient.SteamID = steamId; - pendingClient.Connection.Name = name; + + pendingClient.Connection.SetAccountInfo(new AccountInfo(steamId.Select(uid => (AccountId)uid))); pendingClient.Name = name; pendingClient.OwnerKey = ownKey; pendingClient.AuthSessionStarted = true; @@ -469,7 +484,7 @@ namespace Barotrauma.Networking } else { - if (pendingClient.SteamID != steamId) + if (pendingClient.AccountInfo.AccountId != steamId.Select(uid => (AccountId)uid)) { RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index bdd1fc790..f70f77724 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Text; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -10,7 +9,7 @@ namespace Barotrauma.Networking { 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 InitializationCompleteCallback(NetworkConnection connection, string clientName); public delegate void ShutdownCallback(); public delegate void OwnerDeterminedCallback(NetworkConnection connection); @@ -20,7 +19,7 @@ namespace Barotrauma.Networking public ShutdownCallback OnShutdown; public OwnerDeterminedCallback OnOwnerDetermined; - protected int? ownerKey; + protected Option ownerKey; public NetworkConnection OwnerConnection { get; protected set; } @@ -33,43 +32,23 @@ namespace Barotrauma.Networking protected class PendingClient { public string Name; - public int OwnerKey; + public Option OwnerKey; public 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; @@ -101,7 +80,10 @@ namespace Barotrauma.Networking case ConnectionInitialization.SteamTicketAndVersion: string name = Client.SanitizeName(inc.ReadString()); int ownerKey = inc.ReadInt32(); - UInt64 steamId = inc.ReadUInt64(); + UInt64 steamIdVal = inc.ReadUInt64(); + Option steamId = steamIdVal != 0 + ? Option.Some(new SteamId(steamIdVal)) + : Option.None(); UInt16 ticketLength = inc.ReadUInt16(); byte[] ticketBytes = inc.ReadBytes(ticketLength); @@ -136,7 +118,7 @@ namespace Barotrauma.Networking if (!pendingClient.AuthSessionStarted) { - ProcessAuthTicket(name, ownerKey, steamId, pendingClient, ticketBytes); + ProcessAuthTicket(name, ownerKey != 0 ? Option.Some(ownerKey) : Option.None(), steamId, pendingClient, ticketBytes); } break; case ConnectionInitialization.Password: @@ -172,34 +154,37 @@ namespace Barotrauma.Networking } } - protected abstract void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket); + protected abstract void ProcessAuthTicket(string name, Option ownKey, Option steamId, PendingClient pendingClient, byte[] ticket); 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); - } - else if (pendingClient.Connection is SteamP2PConnection s) - { - serverSettings.BanList.BanPlayer(pendingClient.Name, s.SteamID, banReason, duration); - serverSettings.BanList.BanPlayer(pendingClient.Name, s.OwnerSteamID, banReason, duration); + serverSettings.BanList.BanPlayer(pendingClient.Name, accountId, banReason, duration); } + + if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var id)) { banAccountId(id); } + pendingClient.AccountInfo.OtherMatchingIds.ForEach(banAccountId); + serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.Connection.Endpoint, banReason, duration); } - + 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); + banReason = default; + 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); @@ -225,7 +210,7 @@ namespace Barotrauma.Networking CheckOwnership(pendingClient); - OnInitializationComplete?.Invoke(newConnection); + OnInitializationComplete?.Invoke(newConnection, pendingClient.Name); } pendingClient.TimeOut -= Timing.Step; @@ -244,8 +229,6 @@ namespace Barotrauma.Networking 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++) @@ -286,11 +269,10 @@ namespace Barotrauma.Networking 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; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 8718bc4d5..e33afdbb9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Net; using System.Linq; -using System.Threading; namespace Barotrauma.Networking { @@ -10,29 +8,25 @@ namespace Barotrauma.Networking { private bool started; - public UInt64 OwnerSteamID - { - get; - private set; - } + private readonly SteamId ownerSteamId; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Value); + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - private UInt64 ReadSteamId(IReadMessage inc) - => inc.ReadUInt64() ^ ownerKey64; - private void WriteSteamId(IWriteMessage msg, UInt64 val) - => msg.Write(val ^ ownerKey64); + private SteamId ReadSteamId(IReadMessage inc) + => new SteamId(inc.ReadUInt64() ^ ownerKey64); + private void WriteSteamId(IWriteMessage msg, SteamId val) + => msg.Write(val.Value ^ ownerKey64); - public SteamP2PServerPeer(UInt64 steamId, int ownerKey, ServerSettings settings) + public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings) { serverSettings = settings; connectedClients = new List(); pendingClients = new List(); - this.ownerKey = ownerKey; + this.ownerKey = Option.Some(ownerKey); - OwnerSteamID = steamId; + ownerSteamId = steamId; started = false; } @@ -40,7 +34,7 @@ namespace Barotrauma.Networking public override void Start() { IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, OwnerSteamID); + WriteSteamId(outMsg, ownerSteamId); outMsg.Write((byte)DeliveryMethod.Reliable); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage)); @@ -129,9 +123,9 @@ namespace Barotrauma.Networking { if (!started) { return; } - UInt64 senderSteamId = ReadSteamId(inc); - UInt64 ownerSteamId = ReadSteamId(inc); - + SteamId senderSteamId = ReadSteamId(inc); + SteamId ownerSteamId = ReadSteamId(inc); + PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); if (packetHeader.IsServerMessage()) @@ -140,11 +134,14 @@ namespace Barotrauma.Networking return; } - if (senderSteamId != OwnerSteamID) //sender is remote, handle disconnects and heartbeats + if (senderSteamId != this.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(); @@ -171,7 +168,7 @@ namespace Barotrauma.Networking } else if (connectedClient != null) { - string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={connectedClient.Name}"; + string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == connectedClient).Name}"; Disconnect(connectedClient, disconnectMsg, false); } return; @@ -183,13 +180,9 @@ namespace Barotrauma.Networking } else if (packetHeader.IsConnectionInitializationStep()) { - if (pendingClient != null) { - if (ownerSteamId != 0) - { - pendingClient.Connection.SetOwnerSteamIDIfUnknown(ownerSteamId); - } + pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, ownerSteamId)); ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Buffer, false, inc.BytePosition, inc.LengthBytes - inc.BytePosition, null)); } else @@ -197,7 +190,7 @@ namespace Barotrauma.Networking ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); if (initializationStep == ConnectionInitialization.ConnectionStarted) { - pendingClients.Add(new PendingClient(new SteamP2PConnection("PENDING", senderSteamId)) { SteamID = senderSteamId }); + pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId))); } } } @@ -228,13 +221,13 @@ namespace Barotrauma.Networking if (OwnerConnection == null) { string ownerName = inc.ReadString(); - OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) + OwnerConnection = new SteamP2PConnection(this.ownerSteamId) { Language = GameSettings.CurrentConfig.Language }; - OwnerConnection.SetOwnerSteamIDIfUnknown(OwnerSteamID); + OwnerConnection.SetAccountInfo(new AccountInfo(this.ownerSteamId, this.ownerSteamId)); - OnInitializationComplete?.Invoke(OwnerConnection); + OnInitializationComplete?.Invoke(OwnerConnection, ownerName); } return; } @@ -261,17 +254,19 @@ namespace Barotrauma.Networking { if (!started) { return; } - if (!(conn is SteamP2PConnection steamp2pConn)) return; + if (!(conn is 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.ToString()); return; } + if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || !(connAccountId is SteamId connSteamId)) { 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); + WriteSteamId(msgToSend, connSteamId); msgToSend.Write((byte)deliveryMethod); msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); msgToSend.Write((UInt16)length); @@ -282,7 +277,7 @@ namespace Barotrauma.Networking ChildServerRelay.Write(bufToSend); } - private void SendDisconnectMessage(UInt64 steamId, string msg) + private void SendDisconnectMessage(SteamId steamId, string msg) { if (!started) { return; } if (string.IsNullOrWhiteSpace(msg)) { return; } @@ -303,13 +298,16 @@ namespace Barotrauma.Networking if (!started) { return; } if (!(conn is SteamP2PConnection steamp2pConn)) { return; } - if (sendDisconnectMessage) { SendDisconnectMessage(steamp2pConn.SteamID, msg); } + + if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || !(connAccountId is SteamId connSteamId)) { return; } + + if (sendDisconnectMessage) { SendDisconnectMessage(connSteamId, msg); } if (connectedClients.Contains(steamp2pConn)) { steamp2pConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(steamp2pConn); OnDisconnect?.Invoke(conn, msg); - Steam.SteamManager.StopAuthSession(conn.SteamID); + Steam.SteamManager.StopAuthSession(connSteamId); } else if (steamp2pConn == OwnerConnection) { @@ -324,8 +322,12 @@ namespace Barotrauma.Networking protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) { + var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } + ? id : null; + if (connSteamId is null) { return; } + IWriteMessage msgToSend = new WriteOnlyMessage(); - WriteSteamId(msgToSend, conn.SteamID); + WriteSteamId(msgToSend, connSteamId); msgToSend.Write((byte)deliveryMethod); msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); @@ -333,11 +335,10 @@ namespace Barotrauma.Networking ChildServerRelay.Write(bufToSend); } - protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + protected override void ProcessAuthTicket(string name, Option ownKey, Option steamId, PendingClient pendingClient, byte[] ticket) { pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - pendingClient.Connection.Name = name; pendingClient.Name = name; pendingClient.AuthSessionStarted = true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index c65ba58d2..cfa43ea1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -444,26 +444,28 @@ namespace Barotrauma.Networking } clients[i].Character = character; - character.OwnerClientEndPoint = clients[i].Connection.EndPointString; + character.OwnerClientEndpoint = clients[i].Connection.Endpoint; 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); } } @@ -473,13 +475,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); } } } @@ -489,8 +491,9 @@ namespace Barotrauma.Networking } //try to put the items in containers in the shuttle - foreach (var respawnItem in respawnItems) + 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; } @@ -500,6 +503,7 @@ namespace Barotrauma.Networking break; } } + respawnItems.Add(respawnItem); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 39f7833c9..d39451492 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -69,7 +69,6 @@ namespace Barotrauma.Networking WriteNetProperties(outMsg, c); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); - Whitelist.ServerAdminWrite(outMsg, c); } public void ServerWrite(IWriteMessage outMsg, Client c) @@ -171,7 +170,6 @@ namespace Barotrauma.Networking propertiesChanged |= changedMonsterSettings; if (changedMonsterSettings) { ReadMonsterEnabled(incMsg); } propertiesChanged |= BanList.ServerAdminRead(incMsg, c); - propertiesChanged |= Whitelist.ServerAdminRead(incMsg, c); if (propertiesChanged) { @@ -444,31 +442,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; } @@ -525,69 +519,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")); @@ -595,6 +553,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; @@ -603,23 +562,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/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 0ee0a0d73..c21a8f293 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -257,11 +257,10 @@ namespace Barotrauma if ((DateTime.Now - sender.JoinTime).TotalSeconds > GameMain.Server.ServerSettings.DisallowKickVoteTime) { GameMain.Server.SendDirectChatMessage($"ServerMessage.kickvotedisallowed", sender); - } else { - Client kicked = GameMain.Server.ConnectedClients.Find(c => c.ID == kickedClientID); + Client kicked = GameMain.Server.ConnectedClients.Find(c => c.SessionId == kickedClientID); if (kicked != null && kicked.Connection != GameMain.Server.OwnerConnection && !kicked.HasKickVoteFrom(sender)) { kicked.AddKickVote(sender); @@ -293,9 +292,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 @@ -372,14 +371,14 @@ namespace Barotrauma msg.Write((byte)yesClients.Count()); foreach (Client c in yesClients) { - msg.Write(c.ID); + msg.Write(c.SessionId); } var noClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 1); msg.Write((byte)noClients.Count()); foreach (Client c in noClients) { - msg.Write(c.ID); + msg.Write(c.SessionId); } msg.Write((byte)eligibleClients.Count()); @@ -387,7 +386,7 @@ namespace Barotrauma switch (ActiveVote.State) { case VoteState.Started: - msg.Write(ActiveVote.VoteStarter.ID); + msg.Write(ActiveVote.VoteStarter.SessionId); msg.Write((byte)GameMain.Server.ServerSettings.VoteTimeout); switch (ActiveVote.VoteType) @@ -401,8 +400,8 @@ namespace Barotrauma break; case VoteType.TransferMoney: var transferVote = (ActiveVote as TransferVote); - msg.Write(transferVote.From?.ID ?? 0); - msg.Write(transferVote.To?.ID ?? 0); + msg.Write(transferVote.From?.SessionId ?? 0); + msg.Write(transferVote.To?.SessionId ?? 0); msg.Write(transferVote.TransferAmount); break; } @@ -430,11 +429,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.Write((byte)readyClients.Count()); foreach (Client c in readyClients) { - msg.Write(c.ID); + msg.Write(c.SessionId); } msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 25d6cc5e7..f4a37340a 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 { @@ -58,7 +59,6 @@ namespace Barotrauma.Steam 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("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -76,12 +76,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() + ")"); @@ -90,12 +90,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/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 0b500d712..07f7633bd 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.0.0 + 0.19.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer 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/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 8897ebecb..7ecbbdaa3 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 640973cda..ff815f159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -197,6 +197,10 @@ namespace Barotrauma return; } character.SelectedItem = null; + if (character.SelectedSecondaryItem != null && !character.SelectedSecondaryItem.IsLadder) + { + character.SelectedSecondaryItem = null; + } if (Target is Entity e) { if (e.Removed) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index a0149777f..8c90345cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1234,7 +1234,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; @@ -1243,38 +1243,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); } @@ -1492,12 +1491,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..ee7cdc224 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d6b95ac15..cdc68d100 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -40,7 +40,7 @@ namespace Barotrauma } set { - if (value == enabled) return; + if (value == enabled) { return; } if (Removed) { @@ -63,6 +63,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; @@ -1247,23 +1273,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index aa44f780e..8c37271bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1287,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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index c8dca9919..4aebc88c4 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 4069abe8f..cd6e25f64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -450,13 +450,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 c159dfbe5..e91b9d77e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -909,19 +909,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) { @@ -930,6 +922,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 847ef13d1..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; @@ -720,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; @@ -740,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; @@ -751,6 +755,7 @@ namespace Barotrauma } } } + if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; foreach (DamageModifier damageModifier in tempModifiers) { @@ -763,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/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/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 1d11503a1..6eb09305c 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index a1954e4a8..fc7e77728 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -430,7 +430,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."); @@ -466,7 +466,7 @@ namespace Barotrauma banDuration = parsedBanDuration; } - GameMain.NetworkMember.BanPlayer(clientName, reason, false, banDuration); + GameMain.NetworkMember.BanPlayer(clientName, reason, banDuration); }); }); }, @@ -485,7 +485,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."); @@ -509,12 +509,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, () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 3fe4f0725..8ecedce89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -191,7 +191,6 @@ namespace Barotrauma if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); - newEvent.Init(); ActiveEvents.Add(newEvent); } else @@ -223,6 +222,7 @@ namespace Barotrauma PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; + eventsInitialized = false; isCrewAway = false; crewAwayDuration = 0.0f; crewAwayResetTimer = 0.0f; @@ -460,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)) @@ -490,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()); @@ -614,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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 9e3c000b3..d2770b68b 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 8dae48c00..e76ab4c70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -125,8 +125,7 @@ namespace Barotrauma var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile; if (file == null) { - DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\"."); - disallowed = true; + DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\""); yield break; } else @@ -147,6 +146,32 @@ namespace Barotrauma { DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); } + + monsters = new List(); + + //+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() @@ -487,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)) { @@ -508,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 @@ -533,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) - { - DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\"."); - 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, @@ -587,6 +597,7 @@ namespace Barotrauma value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); } }, delayBetweenSpawns * i); + i++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index b254682e4..7b2e4ab9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -3,7 +3,7 @@ using System; namespace Barotrauma { - public static class StringExtensions + static class StringExtensions { public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 9a0eddeed..a122a213c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -467,11 +467,11 @@ namespace Barotrauma SetConsent(Consent.Error); return; } - loadedImplementation?.SetEnabledInfoLog(true); - loadedImplementation?.SetEnabledVerboseLog(true); #if DEBUG try { + loadedImplementation?.SetEnabledInfoLog(true); + loadedImplementation?.SetEnabledVerboseLog(true); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 7e6bf9e6e..b6868a586 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -133,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; } @@ -228,7 +230,8 @@ namespace Barotrauma #if CLIENT prevCampaignUIAutoOpenType = TransitionType.None; #endif - if (PurchasedHullRepairs) + + if (PurchasedHullRepairsInLatestSave) { foreach (Structure wall in Structure.WallList) { @@ -241,9 +244,9 @@ namespace Barotrauma } } } - PurchasedHullRepairs = false; + PurchasedHullRepairsInLatestSave = PurchasedHullRepairs = false; } - if (PurchasedItemRepairs) + if (PurchasedItemRepairsInLatestSave) { foreach (Item item in Item.ItemList) { @@ -256,9 +259,9 @@ 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); } @@ -725,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 @@ -734,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..32fbcd28a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -151,9 +151,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 76dd68595..0d2f9e1e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -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/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 8e49398a5..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() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 61ab48fe9..89b3f95f7 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.IsAnySelectedItem(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(!picker.IsClimbing, 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index c119ceec5..d7042894f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -96,7 +96,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) @@ -205,18 +205,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; @@ -284,7 +284,6 @@ 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) => 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 6c15619ce..c3a9cb39e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -305,7 +305,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 +371,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; } @@ -616,8 +615,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++; } @@ -732,9 +730,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 f8191d796..3bca94419 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,8 @@ namespace Barotrauma.Items.Components private Vector2 optimalFissionRate, allowedFissionRate; private Vector2 optimalTurbineOutput, allowedTurbineOutput; + private float temperatureBoost; + private bool _powerOn; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] @@ -241,6 +245,34 @@ namespace Barotrauma.Items.Components } #endif + if (signalControlledTargetFissionRate.HasValue && Math.Abs(signalControlledTargetFissionRate.Value - TargetFissionRate) > 1.0f) + { + TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); +#if CLIENT + FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; +#endif + } + else + { + signalControlledTargetFissionRate = null; + } + if (signalControlledTargetTurbineOutput.HasValue && Math.Abs(signalControlledTargetTurbineOutput.Value - TargetTurbineOutput) > 1.0f) + { + 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 +302,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 +473,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 +521,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 @@ -797,30 +834,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); + 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); + registerUnsentChanges(); } break; } + + void registerUnsentChanges() + { + if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; } + } } + + private float? signalControlledTargetFissionRate, signalControlledTargetTurbineOutput; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index e23357700..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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 388b719fc..f123939d7 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index f0dbf6fcb..330c6bd50 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. " + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index a6fee0611..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) { @@ -870,7 +877,7 @@ 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); #if CLIENT @@ -1067,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/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 6d3fbc207..224d1882f 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); @@ -301,6 +312,12 @@ namespace Barotrauma.Items.Components } } } + enumeratingWires = false; + foreach (var removedWire in removedWires) + { + wires.Remove(removedWire); + } + removedWires.Clear(); } public void ClearConnections() @@ -313,13 +330,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/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index b44d136c2..5e4ba1095 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -247,9 +247,14 @@ namespace Barotrauma.Items.Components } public override void OnMapLoaded() + { + CheckIfNeedsUpdate(); + } + + public void CheckIfNeedsUpdate() { if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && IsOn && - (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && + (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { lightBrightness = 1.0f; @@ -261,6 +266,10 @@ namespace Barotrauma.Items.Components Light.ParentSub = item.Submarine; #endif } + else + { + IsActive = true; + } } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index a0b7b75be..d599a3bce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -21,12 +21,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 +97,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 +129,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 +164,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 { @@ -602,9 +636,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 @@ -764,6 +798,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 +816,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 +843,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; @@ -1452,7 +1499,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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 6137938e3..6411b4334 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -99,6 +99,7 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; + private float waterDragCoefficient; public readonly XElement StaticBodyConfig; @@ -860,12 +861,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) @@ -905,6 +908,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": @@ -994,6 +998,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 @@ -1898,10 +1904,19 @@ 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(); + //slow the item down (not physically accurate, but looks good enough) + body.LinearVelocity *= 0.2f; + } + Item container = this.Container; while (!waterProof && container != null) { @@ -1929,7 +1944,8 @@ namespace Barotrauma } } - + partial void Splash(); + public void UpdateTransform() { if (body == null) { return; } @@ -2017,23 +2033,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); + } } @@ -3270,7 +3310,7 @@ namespace Barotrauma relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.RotationRad); Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin; - item.rect.Location -= ((origin - oldOrigin) * scaleRelativeToPrefab).ToPoint(); + item.rect.Location -= (origin - oldOrigin).ToPoint(); } if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index e97bb6481..203d2dbac 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)); @@ -70,6 +73,20 @@ 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 @@ -393,12 +410,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 { @@ -747,7 +858,7 @@ namespace Barotrauma 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 +982,7 @@ namespace Barotrauma { if (!levelCommonness.ContainsKey(levelName)) { - levelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + levelCommonness.Add(levelName, new CommonnessInfo(levelCommonnessElement)); } } else @@ -962,6 +1073,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; 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/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/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index a6f3e2038..6bb343533 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. /// @@ -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,18 +2565,18 @@ 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); - + var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f); + 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++) { - //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(), + 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; } @@ -2554,14 +2586,16 @@ namespace Barotrauma }, 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 abyssResources); + PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0); var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); - abyssClusterLocation.Resources.AddRange(abyssResources); + abyssClusterLocation.Resources.AddRange(placedResources); AbyssResources.Add(abyssClusterLocation); + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); allValidLocations.RemoveAt(locationIndex); - } + } PathPoints.Clear(); nextPathPointId = 0; @@ -2618,17 +2652,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 +2689,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 +2865,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 +2877,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,6 +2922,7 @@ 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)); } } @@ -3041,15 +3066,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); 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/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index a1086d71c..925f45e66 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); @@ -562,7 +562,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 +591,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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index ab8ea3922..f201efb47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -343,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) { @@ -361,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs new file mode 100644 index 000000000..964a4a9ca --- /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.Write(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..12564a692 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -1,37 +1,34 @@ -using Barotrauma.Steam; -using System; +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 +39,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/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index f6f1802ae..ef9030eea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -11,10 +11,10 @@ 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; @@ -28,10 +28,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 +103,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 +120,7 @@ namespace Barotrauma.Networking set { - spectate_position = value.Value; + spectatePos = value.Value; } } @@ -177,19 +190,13 @@ namespace Barotrauma.Networking { 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; + this.SessionId = sessionId; kickVoters = new List(); @@ -234,13 +241,16 @@ namespace Barotrauma.Networking return kickVoters.Contains(voter); } - public bool HasKickVoteFromID(int id) + public bool HasKickVoteFromSessionId(int id) { - return kickVoters.Any(k => k.ID == id); + return kickVoters.Any(k => k.SessionId == id); } + public bool SessionOrAccountIdMatches(string userId) + => (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId) + || (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId); - public static void UpdateKickVotes(List connectedClients) + public static void UpdateKickVotes(IReadOnlyList connectedClients) { foreach (Client client in connectedClients) { @@ -250,7 +260,7 @@ namespace Barotrauma.Networking public void WritePermissions(IWriteMessage msg) { - msg.Write(ID); + msg.Write(SessionId); msg.WriteRangedInteger((int)Permissions, 0, (int)ClientPermissions.All); if (HasPermission(ClientPermissions.ConsoleCommands)) { 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..767c57723 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,368 @@ 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)) + foreach (T e in (T[])Enum.GetValues(type)) { - if (Convert.ChangeType(e, e!.GetTypeCode()) == enumIndex) { return e; } + if ((int)Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; } } throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); } - private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + 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 dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte(); + 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.Write(b); } - private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16(); + 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.Write(b); } - private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16(); + 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.Write(b); } - private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32(); + 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.Write(b); } - 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.Write(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.Write(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.Write(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.Write(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.Write(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.Write(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.Write(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.Write(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 +516,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 = (ReadWriteBehavior)bhvr; + 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 +588,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; + + 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 +648,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 + interface INetSerializableStruct { /// /// Deserializes a network message into a struct. @@ -608,21 +683,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,17 +739,26 @@ 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.Write(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 + static class WriteOnlyMessageExtensions { #if CLIENT public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index afa43e536..7e93a540c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -148,7 +148,6 @@ namespace Barotrauma.Networking InvalidVersion, MissingContentPackage, IncompatibleContentPackage, - NotOnWhitelist, ExcessiveDesyncOldEvent, ExcessiveDesyncRemovedEvent, SyncTimeout, @@ -218,10 +217,7 @@ namespace Barotrauma.Networking get { return gameStarted; } } - public virtual List ConnectedClients - { - get { return null; } - } + public abstract IReadOnlyList ConnectedClients { get; } public RespawnManager RespawnManager { @@ -268,19 +264,22 @@ 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 abstract void UnbanPlayer(string playerName); + + public abstract void UnbanPlayer(Endpoint endpoint); public virtual void Update(float deltaTime) { } - public virtual void Disconnect() { } + public virtual void Quit() { } /// /// Check if the two version are compatible (= if they can play together in multiplayer). 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..3be504afe --- /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..0724a0bca --- /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..3a724de81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs @@ -0,0 +1,69 @@ +#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) + { + NetAddress = netAddress; + } + + public new static Option Parse(string endpointStr) + { + if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint)) + { + return Option.Some(new LidgrenAddress(netEndpoint!)); + } + + try + { + var resolvedAddresses = Dns.GetHostAddresses(endpointStr); + return resolvedAddresses.Any() + ? Option.Some(new LidgrenAddress(resolvedAddresses.First())) + : Option.None(); + } + catch (SocketException) + { + return Option.None(); + } + catch (ArgumentOutOfRangeException) + { + 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..6880848c4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs @@ -0,0 +1,33 @@ +#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) + => 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..32e85d169 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -0,0 +1,63 @@ +#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) + { + if (IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint)) + { + return Option.Some(new LidgrenEndpoint(netEndpoint!)); + } + + if (endpointStr.Count(c => c == ':') == 1) + { + string[] split = endpointStr.Split(':'); + string hostName = split[0]; + if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr) + && int.TryParse(split[1], out var port)) + { + return Option.Some(new LidgrenEndpoint(adr.NetAddress, port)); + } + } + + return LidgrenAddress.Parse(endpointStr) + .Select(adr => new LidgrenEndpoint(adr.NetAddress, NetConfig.DefaultPort)); + } + + 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..65377a23f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - public interface IWriteMessage + interface IWriteMessage { void Write(bool val); void WritePadBits(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 00711ed04..a05e67a59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -11,7 +11,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; @@ -254,6 +254,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); @@ -409,7 +415,7 @@ namespace Barotrauma.Networking } } - public class WriteOnlyMessage : IWriteMessage + class WriteOnlyMessage : IWriteMessage { private byte[] buf = new byte[MsgConstants.InitialBufferSize]; private int seekPos = 0; @@ -602,9 +608,9 @@ namespace Barotrauma.Networking } } - public class ReadOnlyMessage : IReadMessage + class ReadOnlyMessage : IReadMessage { - private byte[] buf; + private readonly byte[] buf; private int seekPos = 0; private int lengthBits = 0; @@ -720,6 +726,11 @@ namespace Barotrauma.Networking return MsgReader.ReadByte(buf, ref seekPos); } + public byte PeekByte() + { + return MsgReader.PeekByte(buf, ref seekPos); + } + public UInt16 ReadUInt16() { return MsgReader.ReadUInt16(buf, ref seekPos); @@ -801,7 +812,7 @@ namespace Barotrauma.Networking } } - public class ReadWriteMessage : IWriteMessage, IReadMessage + class ReadWriteMessage : IWriteMessage, IReadMessage { private byte[] buf; private int seekPos = 0; @@ -983,6 +994,11 @@ namespace Barotrauma.Networking return MsgReader.ReadByte(buf, ref seekPos); } + public byte PeekByte() + { + return MsgReader.PeekByte(buf, ref seekPos); + } + public UInt16 ReadUInt16() { return MsgReader.ReadUInt16(buf, ref seekPos); @@ -1067,5 +1083,6 @@ namespace Barotrauma.Networking { throw new InvalidOperationException("ReadWriteMessages are not to be sent"); } + } } 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/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index c282b31f2..1f1c31596 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -269,6 +269,7 @@ namespace Barotrauma.Networking #endif } } + respawnItems.Clear(); foreach (Structure wall in Structure.WallList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index ec76db8e0..e626f1c23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -70,27 +70,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(); } } @@ -279,7 +270,6 @@ namespace Barotrauma.Networking { ServerLog = new ServerLog(serverName); - Whitelist = new WhiteList(); BanList = new BanList(); ExtraCargo = new Dictionary(); @@ -398,8 +388,6 @@ namespace Barotrauma.Networking public List ClientPermissions { get; private set; } = new List(); - public WhiteList Whitelist { get; private set; } - [Serialize(20, IsPropertySaveable.Yes)] public int TickRate { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs index 7c24c7f02..29411143a 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 5fd149b62..e26cb7e62 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(); @@ -57,7 +57,7 @@ namespace Barotrauma return selected; } - public void ResetVotes(List connectedClients) + public void ResetVotes(IEnumerable connectedClients) { foreach (Client client in connectedClients) { 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/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 89c8bc1bd..ee27a296b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -394,25 +394,6 @@ namespace Barotrauma 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); - } - - return val; - } - public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { var attribute = element?.GetAttribute(name); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 94e6c7bef..ebd8a0b36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -119,6 +119,8 @@ namespace Barotrauma if (!HasRequiredConditions(currentTargets)) { return; } + if (Entity.Spawner != null && Entity.Spawner.IsInRemoveQueue(entity)) { return; } + switch (delayType) { case DelayTypes.Timer: diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 71b24e12a..29f5fe5ba 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."); @@ -1668,11 +1667,11 @@ namespace Barotrauma Character.Controlled = newCharacter; } #elif SERVER - foreach (Client c in GameMain.Server.ConnectedClients) + /*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); } @@ -1705,92 +1704,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.ShowError("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; @@ -2082,7 +2090,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..5eb17570d 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() @@ -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/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/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/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..30a219a32 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 @@ -23,7 +24,7 @@ namespace Barotrauma outValue = value; return true; case None _: - outValue = default; + outValue = default!; return false; default: throw new ArgumentOutOfRangeException(); @@ -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..e02fe1c68 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,52 @@ 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(string str) + { + static Option none() => Option.None(); + + var derivedTypes = GetDerivedNonAbstract(); + + Option parseOfType(Type t) + { + //every T1 type is expected to have a method with the following signature: + // public static Option Parse(string 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 : T1 + => option.Select(v => (T1)v); + Func, Option> f = convert; + var constructedConverter = f.Method.GetGenericMethodDefinition().MakeGenericMethod(typeof(T1), t); + + return constructedConverter.Invoke(null, new object?[] { parseFunc.Invoke(null, new object[] { str }) }) + 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..1a2e8968c 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 { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 9922dc37f..e7a38ef78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -26,14 +26,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 { @@ -712,5 +712,10 @@ namespace Barotrauma return e; } + + public static void ThrowIfNull(T o) + { + if (o is null) { throw new ArgumentNullException(); } + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d3ba6592d..3b27b64c9 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,86 @@ +--------------------------------------------------------------------------------------------------------- +v0.19.1.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Option to lock or reset draggable item UIs. +- Force item UI layout update when resolution changes. Fixes item repositioned item UIs potentially getting left outside the window when switching to a smaller resolution. +- Item UIs can't be dragged under each other. +- Fixed dragging a wire in a connection panel dragging the panel too if you bring the cursor close to the edges of the panel. +- Fixed seated bots being unable to get up from the chair. +- Fixed some more unwired lights in ResearchModule_02_Colony. +- Fixed another minor gap issue in Winterhalter. +- Fixed hull/item repairs and the money spent on them appearing to reset if you join mid-round after repairs have been purchased. +- Fixed light source staying in place after detaching a glowing item (e.g. mineral) from a wall. +- Fixed server trying to place all previously spawned respawn items into containers on every respawn, even if the items have already been removed. Happened because we never cleared respawnItems, and because we used that list when placing new respawn items into containers. + +Changes and additions: +- Overvoltage makes devices perform better, increasing the output of engines, making fabricators, deconstructors and fabricators operate faster, electrical discharge coils do more damage and oxygen generators generate more oxygen. Incentivizes 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 in the ass to deal with and intentionally overvolting the devices more worthwhile. +- Experimental: Added a way to immediately increase/decrease the temperatureof the reactor for a brief amount of time (atm bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and to conserve fuel by operating the reactor at a lower fission rate = serves as another incentive to operate it 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. +- Added a Large Weapon Hardpoint. +- Railgun is now considered a large weapon. +- Added Flak Cannon and Double Coilgun as large weapons. +- Pulse Laser and Railgun now have similar power consumption as other turrets. +- 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. + +Balance: +- Reduced the pulse laser tri-laser bolt spread. +- Explosions are now calculated differently, using the number of limbs to divide the damage (to a max of 15 limbs). Adjusted explosion damage values to match new calculations. +- Coilgun costs 5000 to install, Pulse Laser and Chaingun 6000. Large turrets all cost 7500. +- Make mudraptor eggs slightly viable for farming (decreased cost from shop, increased deconstruction yields) +- Mineral yield and spawn rates rebalance, minerals found are now much more dependant on location (biome, cave, abyss) + +Submarines: +- Added a new intermediate transport sub, Camel. Still WIP, but feedback is welcome. +- Fixed messy wiring in Typhon 2's bottom left hardpoint. +- Added some loose vents and panels to Herja, Winterhalter and Barsuk, fixed invisible "loose panel" (news stand) in Orca 2. + +Fixes: +- Fixed brief freezes when monsters spawn mid-round. +- 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 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 level resource spawn rate not properly respecting level generations parameters' resource spawn chance values. +- 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 get 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. 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 cabs 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 sub 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. +- Don't stop selecting text in a textbox if the cursor goes outside the box. + +Multiplayer: +- 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 gotten 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. + +Submarine editor: +- 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 selection rectangle now works even if cursor reaches into the prefab list, letting go of a dragged entity works even if cursor reaches into the prefab list, 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 sub editor's command history, preventing undoing it. + +Modding: +- 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). +- Fixed affliction names and descriptions being empty if they're not available in the selected language nor configured in the affliction xml directly. +- Fix custom infected husks seeking for any husk infection targeting the matching species instead of checking that also the husk affliction prefab matches the husk. + --------------------------------------------------------------------------------------------------------- v0.19.0.0 --------------------------------------------------------------------------------------------------------- @@ -35,6 +118,8 @@ Balance: - Adjusted supplies in pirate submarines. - Turn some weapons' burn damage into explosion damage (wip). - Terminal ignores empty signals. +- Reduce commonness of Molochs (as they can take a lot of time to kill, running into multiple of them can quickly become a chore) +- Remove steel requirement for depth charges. Fabricate decoy depth charges from depth charge, rather than base material. Multiplayer: - 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.