From 1fd2a51bbb1b44bac9ff3606f697bfa96142e7af Mon Sep 17 00:00:00 2001 From: Juan Pablo Arce Date: Wed, 14 Sep 2022 12:47:17 -0300 Subject: [PATCH] Unstable v0.19.5.0 --- .../ClientSource/Characters/CharacterHUD.cs | 51 +- .../ClientSource/Characters/CharacterInfo.cs | 7 +- .../ContentPackage/ModProject.cs | 12 +- .../ContentPackageManager.cs | 8 +- ...nsition.cs => LegacySteamUgcTransition.cs} | 8 +- .../ClientSource/DebugConsole.cs | 30 +- .../Events/EventActions/MessageBoxAction.cs | 73 + .../EventActions/TutorialHighlightAction.cs | 51 + .../EventActions/TutorialSegmentAction.cs | 50 + .../ClientSource/Fonts/ScalableFont.cs | 114 +- .../ClientSource/GUI/GUIComponent.cs | 4 +- .../ClientSource/GUI/GUIDropDown.cs | 27 +- .../ClientSource/GUI/GUIMessageBox.cs | 35 +- .../ClientSource/GUI/GUIPrefab.cs | 6 +- .../ClientSource/GUI/GUIStyle.cs | 2 +- .../ClientSource/GUI/GUITickBox.cs | 2 +- .../ClientSource/GUI/RectTransform.cs | 17 +- .../ClientSource/GUI/SubmarineSelection.cs | 17 +- .../ClientSource/GUI/TabMenu.cs | 15 +- .../ClientSource/GUI/UpgradeStore.cs | 48 +- .../BarotraumaClient/ClientSource/GameMain.cs | 8 +- .../ClientSource/GameSession/CrewManager.cs | 6 +- .../GameModes/Tutorials/Tutorial.cs | 199 +- .../Items/Components/Holdable/RangedWeapon.cs | 2 +- .../Items/Components/ItemComponent.cs | 1 + .../Items/Components/Machines/Sonar.cs | 13 + .../Items/Components/Repairable.cs | 19 +- .../ClientSource/Items/Item.cs | 2 +- .../ClientSource/Map/Structure.cs | 2 +- .../ClientSource/Map/SubmarineInfo.cs | 9 +- .../ClientSource/Networking/Client.cs | 8 + .../ClientSource/Networking/GameClient.cs | 704 +++--- .../Networking/Primitives/Peers/ClientPeer.cs | 103 +- .../Primitives/Peers/LidgrenClientPeer.cs | 18 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 59 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 64 +- .../ClientSource/Networking/ServerInfo.cs | 517 ----- .../FriendProviders/FriendProvider.cs | 11 + .../FriendProviders/SteamFriendProvider.cs | 67 + .../Networking/ServerList/PingUtils.cs | 196 ++ .../Networking/ServerList/ServerInfo.cs | 509 ++++ .../CompositeServerProvider.cs | 35 + .../ServerProviders/ServerProvider.cs | 17 + .../SteamDedicatedServerProvider.cs | 160 ++ .../ServerProviders/SteamP2PServerProvider.cs | 107 + .../ClientSource/Networking/ServerSettings.cs | 14 +- .../ClientSource/Networking/Voting.cs | 18 + .../ClientSource/Screens/MainMenuScreen.cs | 47 +- .../ClientSource/Screens/ModDownloadScreen.cs | 60 +- .../ClientSource/Screens/NetLobbyScreen.cs | 28 +- .../ClientSource/Screens/ServerListScreen.cs | 2044 ----------------- .../Screens/ServerListScreen/PanelAnimator.cs | 112 + .../ServerListScreen/ServerListScreen.cs | 1676 ++++++++++++++ .../ClientSource/Screens/SubEditorScreen.cs | 247 +- .../Serialization/SerializableEntityEditor.cs | 5 + .../ClientSource/Sprite/Sprite.cs | 17 +- .../ClientSource/Steam/BulkDownloader.cs | 4 +- .../ClientSource/Steam/Lobby.cs | 274 +-- .../ClientSource/Steam/Workshop.cs | 16 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 26 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 16 +- .../WorkshopMenu/Mutable/ModListPreset.cs | 18 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 24 +- .../Text/LocalizedString/WrappedLString.cs | 2 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 3 +- .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameModes/CharacterCampaignData.cs | 22 +- .../GameModes/MultiPlayerCampaign.cs | 15 +- .../ServerSource/Networking/BanList.cs | 8 + .../ServerSource/Networking/Client.cs | 40 +- .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 440 ++-- .../ServerEntityEventManager.cs | 6 +- .../Networking/OrderChatMessage.cs | 2 +- .../Peers/Server/LidgrenServerPeer.cs | 60 +- .../Primitives/Peers/Server/ServerPeer.cs | 66 +- .../Peers/Server/SteamP2PServerPeer.cs | 72 +- .../ServerSource/Networking/RespawnManager.cs | 5 +- .../ServerSource/Networking/ServerSettings.cs | 38 +- .../ServerSource/Networking/Voting.cs | 8 + .../ServerSource/Screens/NetLobbyScreen.cs | 2 +- .../ServerSource/Steam/SteamManager.cs | 7 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/IndoorsSteeringManager.cs | 4 +- .../SharedSource/Characters/CharacterInfo.cs | 4 +- .../Characters/CharacterPrefab.cs | 4 - .../SharedSource/Characters/CorpsePrefab.cs | 8 +- .../Characters/Health/CharacterHealth.cs | 3 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 8 +- .../Characters/Talents/TalentPrefab.cs | 7 +- .../ContentFile/GenericPrefabFile.cs | 4 +- .../ContentPackage/ContentPackage.cs | 37 +- .../ContentPackageId/ContentPackageId.cs | 19 + .../ContentPackageId/SteamWorkshopId.cs | 32 + .../ContentPackageManager.cs | 4 +- .../ContentManagement/ContentPath.cs | 8 +- .../SharedSource/CoroutineManager.cs | 13 + .../SharedSource/DebugConsole.cs | 2 + .../EventActions/CheckConditionalAction.cs | 5 +- .../EventActions/CheckConnectionAction.cs | 65 + .../Events/EventActions/CheckItemAction.cs | 25 +- .../Events/EventActions/CheckOrderAction.cs | 40 +- .../EventActions/CheckSelectedItemAction.cs | 89 + .../Events/EventActions/CheckTalentAction.cs | 49 + .../Events/EventActions/MessageBoxAction.cs | 91 +- .../Events/EventActions/TeleportAction.cs | 48 + .../EventActions/TutorialHighlightAction.cs | 27 + .../Events/EventActions/TutorialIconAction.cs | 65 + .../EventActions/TutorialSegmentAction.cs | 65 +- .../SharedSource/Events/EventSet.cs | 10 +- .../Extensions/IEnumerableExtensions.cs | 10 + .../Extensions/StringExtensions.cs | 17 +- .../GameModes/Tutorials/TutorialPrefab.cs | 15 +- .../Items/Components/Machines/Sonar.cs | 2 + .../BooleanOperatorComponent.cs | 9 +- .../Items/Components/Signal/WaterDetector.cs | 3 + .../SharedSource/Items/Item.cs | 3 +- .../SharedSource/Items/ItemPrefab.cs | 20 +- .../SharedSource/Map/CoreEntityPrefab.cs | 6 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 2 +- .../SharedSource/Map/Levels/CaveGenerator.cs | 4 +- .../SharedSource/Map/LinkedSubmarine.cs | 10 +- .../SharedSource/Map/Map/Location.cs | 2 +- .../SharedSource/Map/MapEntityPrefab.cs | 24 + .../SharedSource/Map/StructurePrefab.cs | 23 +- .../SharedSource/Map/Submarine.cs | 1 + .../SharedSource/Networking/Client.cs | 10 +- .../SharedSource/Networking/NetworkMember.cs | 82 +- .../Primitives/AccountId/AccountId.cs | 2 +- .../Networking/Primitives/Address/Address.cs | 2 +- .../Primitives/Address/LidgrenAddress.cs | 32 +- .../Primitives/Endpoint/Endpoint.cs | 17 +- .../Primitives/Endpoint/LidgrenEndpoint.cs | 27 +- .../Primitives/NetworkPeerStructs.cs | 175 +- .../SharedSource/Networking/RespawnManager.cs | 8 +- .../SharedSource/Networking/ServerSettings.cs | 5 +- .../SharedSource/Networking/Voting.cs | 18 - .../SharedSource/Prefabs/Prefab.cs | 2 +- .../SharedSource/Prefabs/PrefabCollection.cs | 8 + .../SharedSource/Prefabs/PrefabSelector.cs | 72 +- .../Serialization/XMLExtensions.cs | 19 + .../SharedSource/Settings/GameSettings.cs | 4 +- .../StatusEffects/DelayedEffect.cs | 2 - .../SharedSource/Steam/Workshop.cs | 16 +- .../SharedSource/Text/TextManager.cs | 48 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 21 +- .../SharedSource/Utils/Option/Option.cs | 10 +- .../SharedSource/Utils/ReflectionUtils.cs | 29 +- .../SharedSource/Utils/Threading.cs | 34 + Barotrauma/BarotraumaShared/changelog.txt | 85 +- .../Facepunch.Steamworks/ServerList/Base.cs | 10 +- .../Facepunch.Steamworks/Structs/Friend.cs | 6 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 14 +- 158 files changed, 5702 insertions(+), 4813 deletions(-) rename Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/{UgcTransition.cs => LegacySteamUgcTransition.cs} (97%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index a605b41c6..9ef84bc9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Tutorials; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -398,36 +399,58 @@ namespace Barotrauma progressBar.Draw(spriteBatch, cam); } - foreach (Character npc in Character.CharacterList) + void DrawInteractionIcon(Entity entity, string iconStyle) { - if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } + if (entity == null || entity.Removed) { return; } + var characterEntity = entity as Character; + if (characterEntity is not null && (characterEntity.IsDead || characterEntity.IsIncapacitated)) { return; } + if (GUIStyle.GetComponentStyle(iconStyle) is not GUIComponentStyle style) { return; } - var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); - if (iconStyle == null) { continue; } - Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); - if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) + Hull currentHull = entity switch + { + Character character => character.CurrentHull, + Item item => item.CurrentHull, + _ => null + }; + + Range visibleRange = new Range(currentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); + if (characterEntity?.CampaignInteractionType == CampaignMode.InteractionType.Examine) { //TODO: we could probably do better than just hardcoding //a check for InteractionType.Examine here. - if (Vector2.DistanceSquared(character.Position, npc.Position) > 500f * 500f) { continue; } + if (Vector2.DistanceSquared(character.Position, entity.Position) > 500f * 500f) { return; } - var body = Submarine.CheckVisibility(character.SimPosition, npc.SimPosition, ignoreLevel: true); - if (body != null && body.UserData as Character != npc) { continue; } + var body = Submarine.CheckVisibility(character.SimPosition, entity.SimPosition, ignoreLevel: true); + if (body != null && body.UserData != entity) { return; } visibleRange = new Range(-100f, 500f); } - float dist = Vector2.Distance(character.WorldPosition, npc.WorldPosition); + float dist = Vector2.Distance(character.WorldPosition, entity.WorldPosition); float distFactor = 1.0f - MathUtils.InverseLerp(1000.0f, 3000.0f, dist); float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor); GUI.DrawIndicator( spriteBatch, - npc.WorldPosition, + entity.WorldPosition, cam, visibleRange, - iconStyle.GetDefaultSprite(), - iconStyle.Color * alpha, - label: npc.Info?.Title); + style.GetDefaultSprite(), + style.Color * alpha, + label: characterEntity?.Info?.Title); + } + + foreach (Character npc in Character.CharacterList) + { + if (npc.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } + DrawInteractionIcon(npc, "CampaignInteractionIcon." + npc.CampaignInteractionType); + } + + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial is not null) + { + foreach (var (entity, iconStyle) in tutorialMode.Tutorial.Icons) + { + DrawInteractionIcon(entity, iconStyle); + } } foreach (Item item in Item.ItemList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 290746f93..ab2942b42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -521,8 +521,9 @@ namespace Barotrauma Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); - string ragdollFile = inc.ReadString(); + string ragdollFile = inc.ReadString(); + Identifier npcId = inc.ReadIdentifier(); uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); @@ -539,9 +540,9 @@ namespace Barotrauma } // TODO: animations - CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant) + CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { - ID = infoID, + ID = infoID }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 34d2a3a75..85877787b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -63,7 +63,7 @@ namespace Barotrauma files = contentPackage.Files.Select(File.FromContentFile).ToList(); ModVersion = IncrementModVersion(contentPackage.ModVersion); IsCore = contentPackage is CorePackage; - SteamWorkshopId = contentPackage.SteamWorkshopId; + UgcId = contentPackage.UgcId; ExpectedHash = contentPackage.Hash; InstallTime = contentPackage.InstallTime; } @@ -90,9 +90,9 @@ namespace Barotrauma public bool IsCore = false; - public UInt64 SteamWorkshopId = 0; + public Option UgcId = Option.None(); - public DateTime? InstallTime = null; + public Option InstallTime = Option.None(); public bool HasFile(File file) => Files.Any(f => @@ -120,7 +120,7 @@ namespace Barotrauma public void DiscardHashAndInstallTime() { ExpectedHash = null; - InstallTime = null; + InstallTime = Option.None(); } public static string IncrementModVersion(string modVersion) @@ -155,11 +155,11 @@ namespace Barotrauma addRootAttribute("name", Name); if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } addRootAttribute("corepackage", IsCore); - if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } + if (UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId steamWorkshopId) { addRootAttribute("steamworkshopid", steamWorkshopId.Value); } addRootAttribute("gameversion", GameMain.Version); if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } - if (InstallTime != null) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)); } + if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); } files.ForEach(f => rootElement.Add(f.ToXElement())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 86a971bf5..e59354ecb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -45,9 +45,11 @@ namespace Barotrauma var needInstalling = subscribedItems.Where(item => !WorkshopPackages.Any(p - => item.Id == p.SteamWorkshopId - && p.InstallTime.HasValue - && item.LatestUpdateTime <= p.InstallTime)) + => p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && item.Id == workshopId.Value + && p.InstallTime.TryUnwrap(out var installTime) + && item.LatestUpdateTime <= installTime)) .ToArray(); if (needInstalling.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs similarity index 97% rename from Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs rename to Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs index 748995b91..2b766254c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Transition /// Class dedicated to transitioning away from the old, shitty /// Mods + Submarines folders to the new LocalMods folder /// - public static class UgcTransition + public static class LegacySteamUgcTransition { private const string readmeName = "LOCALMODS_README.txt"; @@ -168,7 +168,11 @@ namespace Barotrauma.Transition addHeader(TextManager.Get("SubscribedMods")); foreach (var mod in mods.Mods) { - addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id)); + addTickbox(mod.Dir, mod.Name, + ticked: !(mod.Item is { } item && ContentPackageManager.LocalPackages.Any(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == item.Id))); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 98fdaa449..bfa8a5194 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -2489,26 +2489,8 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); + #if DEBUG - commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) => - { - VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null); - })); - - commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => - { - TaskPool.Add("DebugQueryLobbies", - SteamManager.LobbyQueryRequest(), (t) => - { - t.TryGetResult(out List lobbies); - foreach (var lobby in lobbies) - { - NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow); - } - NewMessage($"Retrieved a total of {lobbies.Count} lobbies", Color.Lime); - }); - })); - commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { if (args.Length != 1) { return; } @@ -3056,11 +3038,6 @@ namespace Barotrauma commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args => { - if (GameMain.GameSession != null) - { - ThrowError("Using the command is not allowed during an active game session: the command is intended to be used in the character editor or in the main menu."); - return; - } var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true); if (character == null) { @@ -3072,11 +3049,6 @@ namespace Barotrauma commands.Add(new Command("loadwearable", "Force select certain variant for the selected character.", args => { - if (GameMain.GameSession != null) - { - ThrowError("Using the command is not allowed during an active game session: the command is intended to be used in the character editor or in the main menu."); - return; - } var character = Character.Controlled; if (character == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs new file mode 100644 index 000000000..e2bd6f963 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -0,0 +1,73 @@ +using Barotrauma.Tutorials; +using System; +using System.Linq; + +namespace Barotrauma; + +partial class MessageBoxAction : EventAction +{ + partial void UpdateProjSpecific() + { + if (Type == ActionType.Create) + { + CreateMessageBox(); + if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + Identifier id = Identifier.IsEmpty ? Text : Identifier; + var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); + tutorialMode.Tutorial?.TriggerTutorialSegment(segment); + } + } + else if (Type == ActionType.Close) + { + GUIMessageBox.Close(Tag); + } + } + + public void CreateMessageBox() + { + new GUIMessageBox( + headerText: TextManager.Get(Header), + text: RichString.Rich(TextManager.ParseInputTypes(TextManager.Get(Text).Fallback(Text.ToString()), useColorHighlight: true)), + buttons: Array.Empty(), + type: GUIMessageBox.Type.Tutorial, + tag: Tag, + iconStyle: IconStyle, + autoCloseCondition: GetAutoCloseCondition(), + hideCloseButton: HideCloseButton) + { + FlashOnAutoCloseCondition = true + }; + } + + private Func GetAutoCloseCondition() + { + var character = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Character; + Func autoCloseCondition = null; + if (!string.IsNullOrEmpty(CloseOnInput) && Enum.TryParse(CloseOnInput, true, out InputType closeOnInput)) + { + autoCloseCondition = () => PlayerInput.KeyDown(closeOnInput); + } + else if (!CloseOnSelectTag.IsEmpty) + { + autoCloseCondition = () => character?.SelectedItem != null && character.SelectedItem.HasTag(CloseOnSelectTag); + } + else if (!CloseOnPickUpTag.IsEmpty) + { + autoCloseCondition = () => character?.Inventory != null && character.Inventory.FindItemByTag(CloseOnPickUpTag, recursive: true) != null; + } + else if (!CloseOnEquipTag.IsEmpty) + { + autoCloseCondition = () => character != null && character.HasEquippedItem(CloseOnEquipTag); + } + else if (!CloseOnExitRoomName.IsEmpty) + { + autoCloseCondition = () => character?.CurrentHull == null || character.CurrentHull.RoomName.ToIdentifier() != CloseOnExitRoomName; + } + else if (!CloseOnInRoomName.IsEmpty) + { + autoCloseCondition = () => character?.CurrentHull != null && character.CurrentHull.RoomName.ToIdentifier() == CloseOnInRoomName; + } + return autoCloseCondition; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs new file mode 100644 index 000000000..beb14498a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs @@ -0,0 +1,51 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +partial class TutorialHighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.OrangeRed; + + partial void UpdateProjSpecific() + { + if (GameMain.GameSession?.GameMode is not TutorialMode) { return; } + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + SetHighlight(target); + } + } + + private void SetHighlight(Entity entity) + { + if (entity is Item i) + { + SetHighlight(i); + } + else if (entity is Structure s) + { + SetHighlight(s); + } + else if (entity is Character c) + { + SetHighlight(c); + } + } + + private void SetHighlight(Item item) + { + if (item.ExternalHighlight == State) { return; } + item.SpriteColor = (State) ? highlightColor : Color.White; + item.ExternalHighlight = State; + } + + private void SetHighlight(Structure structure) + { + structure.SpriteColor = (State) ? highlightColor : Color.White; + structure.ExternalHighlight = State; + } + + private void SetHighlight(Character character) + { + character.ExternalHighlight = State; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs new file mode 100644 index 000000000..d39340d05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs @@ -0,0 +1,50 @@ +using Barotrauma.Tutorials; + +namespace Barotrauma; + +partial class TutorialSegmentAction : EventAction +{ + private Tutorial.Segment segment; + + partial void UpdateProjSpecific() + { + // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) + if (Type == SegmentActionType.Trigger) + { + segment = Tutorial.Segment.CreateInfoBoxSegment(Id, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), + new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); + } + else if (Type == SegmentActionType.Add) + { + segment = Tutorial.Segment.CreateObjectiveSegment(Id, !ObjectiveTag.IsEmpty ? ObjectiveTag : Id); + } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + if (tutorialMode.Tutorial is Tutorial tutorial) + { + switch (Type) + { + case SegmentActionType.Trigger: + case SegmentActionType.Add: + tutorial.TriggerTutorialSegment(segment); + break; + case SegmentActionType.Complete: + tutorial.CompleteTutorialSegment(Id); + break; + case SegmentActionType.Remove: + tutorial.RemoveTutorialSegment(Id); + break; + case SegmentActionType.CompleteAndRemove: + tutorial.CompleteTutorialSegment(Id); + tutorial.RemoveTutorialSegment(Id); + break; + } + } + } + else + { + DebugConsole.ShowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index ae0a54609..b7b05c7f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -6,23 +6,26 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; +using System.Threading; +using Barotrauma.Threading; namespace Barotrauma { public class ScalableFont : IDisposable { - private static List FontList = new List(); + private static readonly List FontList = new List(); private static Library Lib = null; - private readonly object mutex = new object(); + private static readonly object globalMutex = new object(); + + private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(); - private string filename; - private Face face; + private readonly string filename; + private readonly Face face; private uint size; private int baseHeight; - private Dictionary texCoords; - private List textures; - private GraphicsDevice graphicsDevice; + private readonly Dictionary texCoords; + private readonly List textures; + private readonly GraphicsDevice graphicsDevice; private Vector2 currentDynamicAtlasCoords; private int currentDynamicAtlasNextY; @@ -49,7 +52,7 @@ namespace Barotrauma set { size = value; - if (graphicsDevice != null) RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); + if (graphicsDevice != null) { RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); } } } @@ -93,11 +96,15 @@ namespace Barotrauma public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false) { - lock (mutex) + lock (globalMutex) + { + Lib ??= new Library(); + } + + this.filename = filename; + this.face = null; + using (new ReadLock(rwl)) { - if (Lib == null) Lib = new Library(); - this.filename = filename; - this.face = null; foreach (ScalableFont font in FontList) { if (font.filename == filename) @@ -106,19 +113,23 @@ namespace Barotrauma break; } } - this.face ??= new Face(Lib, filename); - this.size = size; - this.textures = new List(); - this.texCoords = new Dictionary(); - this.DynamicLoading = dynamicLoading; - this.IsCJK = isCJK; - this.graphicsDevice = gd; + } - if (gd != null && !dynamicLoading) - { - RenderAtlas(gd); - } + this.face ??= new Face(Lib, filename); + this.size = size; + this.textures = new List(); + this.texCoords = new Dictionary(); + this.DynamicLoading = dynamicLoading; + this.IsCJK = isCJK; + this.graphicsDevice = gd; + if (gd != null && !dynamicLoading) + { + RenderAtlas(gd); + } + + lock (globalMutex) + { FontList.Add(this); } } @@ -162,7 +173,7 @@ namespace Barotrauma Vector2 currentCoords = Vector2.Zero; int nextY = 0; - lock (mutex) + using (new WriteLock(rwl)) { face.SetPixelSizes(0, size); face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal); @@ -175,19 +186,22 @@ namespace Barotrauma for (uint j = start; j <= end; j++) { uint glyphIndex = face.GetCharIndex(j); - if (glyphIndex == 0) continue; + if (glyphIndex == 0) + { + texCoords.Add(j, new GlyphData( + advance: 0, + texIndex: -1)); + continue; + } face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) - { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty + //glyph is empty, but char might still apply advance + GlyphData blankData = new GlyphData( + advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(j, blankData); - } + texCoords.Add(j, blankData); continue; } //stacktrace doesn't really work that well when RenderGlyph throws an exception @@ -257,7 +271,7 @@ namespace Barotrauma private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) { bool missingCharacterFound = false; - lock (mutex) + using (new ReadLock(rwl)) { missingCharacterFound = !texCoords.ContainsKey(character); } @@ -268,10 +282,9 @@ namespace Barotrauma private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54) { bool missingCharacterFound = false; - var distinctChrs = str.Distinct().Select(c => (uint)c).ToArray(); - lock (mutex) + using (new ReadLock(rwl)) { - foreach (var character in distinctChrs) + foreach (var character in str) { if (texCoords.ContainsKey(character)) { continue; } @@ -280,7 +293,7 @@ namespace Barotrauma } } if (!missingCharacterFound) { return; } - DynamicRenderAtlas(gd, distinctChrs, texDims, baseChar); + DynamicRenderAtlas(gd, str.Select(c => (uint)c), texDims, baseChar); } private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable characters, int texDims = 1024, uint baseChar = 0x54) @@ -299,7 +312,7 @@ namespace Barotrauma Fixed26Dot6 horizontalAdvance; Vector2 drawOffset; - lock (mutex) + using (new WriteLock(rwl)) { if (textures.Count == 0) { @@ -318,20 +331,23 @@ namespace Barotrauma if (texCoords.ContainsKey(character)) { continue; } uint glyphIndex = face.GetCharIndex(character); - if (glyphIndex == 0) { continue; } + if (glyphIndex == 0) + { + texCoords.Add(character, new GlyphData( + advance: 0, + texIndex: -1)); + continue; + } face.SetPixelSizes(0, size); face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) - { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(character, blankData); - } + //glyph is empty, but char might still apply advance + GlyphData blankData = new GlyphData( + advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + texIndex: -1); //indicates no texture because the glyph is empty + texCoords.Add(character, blankData); continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 764382577..73ba38a9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -730,7 +730,7 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) { return; } - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect); + DrawToolTip(spriteBatch, ToolTip, Rect); } public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) @@ -781,7 +781,7 @@ namespace Barotrauma if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { toolTipBlock.RectTransform.AbsoluteOffset -= new Point( - (targetElement.Width / 2) * Math.Sign(targetElement.Center.X - toolTipBlock.Center.X), + 0, toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); } toolTipBlock.SetTextPos(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 59510e874..a8ca188cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -204,15 +204,7 @@ namespace Barotrauma currentHighestParent = FindHighestParent(); currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; - rectT.ParentChanged += (RectTransform newParent) => - { - currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; - if (newParent != null) - { - currentHighestParent = FindHighestParent(); - currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; - } - }; + rectT.ParentChanged += _ => RefreshListBoxParent(); } @@ -396,6 +388,15 @@ namespace Barotrauma return true; } + public void RefreshListBoxParent() + { + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; + if (RectTransform.Parent == null) { return; } + + currentHighestParent = FindHighestParent(); + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; + } + private void AddListBoxToGUIUpdateList(GUIComponent parent) { //the parent is not our parent anymore :( @@ -403,11 +404,13 @@ namespace Barotrauma //and somewhere between this component and the higher parent a component was removed for (int i = 1; i < parentHierarchy.Count; i++) { - if (!parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false)) + if (parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false)) { - parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; - return; + continue; } + + parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; + return; } if (Dropped) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index f7bad7dc1..b67fd7713 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -34,7 +34,7 @@ namespace Barotrauma public GUIFrame InnerFrame { get; private set; } public GUITextBlock Header { get; private set; } public GUITextBlock Text { get; private set; } - public string Tag { get; private set; } + public Identifier Tag { get; private set; } public bool Closed { get; private set; } public bool DisplayInLoadingScreens; @@ -78,6 +78,8 @@ namespace Barotrauma /// private readonly Func autoCloseCondition; + public bool FlashOnAutoCloseCondition { get; set; } + public Type MessageBoxType => type; public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); @@ -142,7 +144,7 @@ namespace Barotrauma } GUIStyle.Apply(InnerFrame, "", this); this.type = type; - Tag = tag; + Tag = tag.ToIdentifier(); #warning TODO: These should be broken into separate methods at least if (type == Type.Default || type == Type.Vote) @@ -199,6 +201,7 @@ namespace Barotrauma var button = new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f / buttons.Length), buttonContainer.RectTransform), buttons[i]); Buttons.Add(button); } + GUITextBlock.AutoScaleAndNormalize(Buttons.Select(btn => btn.TextBlock)); } else if (type == Type.InGame || type == Type.Tutorial) { @@ -540,9 +543,7 @@ namespace Barotrauma if (type == Type.InGame || type == Type.Tutorial) { initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); - defaultPos = type == Type.InGame ? - new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale) : - new Vector2(0.0f, GameMain.GraphicsHeight / 2); + defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); } else @@ -574,10 +575,18 @@ namespace Barotrauma inGameCloseTimer += deltaTime; } - if (inGameCloseTimer >= inGameCloseTime || (autoCloseCondition != null && autoCloseCondition())) + if (inGameCloseTimer >= inGameCloseTime) { Close(); } + else if (autoCloseCondition != null && autoCloseCondition()) + { + Close(); + if (FlashOnAutoCloseCondition) + { + InnerFrame.Flash(GUIStyle.Green); + } + } } else { @@ -634,7 +643,6 @@ namespace Barotrauma } } - public void Close() { if (IsAnimated) @@ -662,6 +670,19 @@ namespace Barotrauma MessageBoxes.Clear(); } + public static void Close(Identifier tag) + { + foreach (var messageBox in MessageBoxes) + { + if (messageBox is GUIMessageBox mb && mb.Tag == tag) + { + mb.Close(); + } + } + } + + public static void Close(string tag) => Close(tag.ToIdentifier()); + /// /// Parent does not matter. It's overridden. /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 137eee850..8ae5366b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -178,19 +178,19 @@ namespace Barotrauma { public GUIFont(string identifier) : base(identifier) { } - public bool HasValue => Prefabs.Any(); + public bool HasValue => !Prefabs.IsEmpty; public ScalableFont Value => Prefabs.ActivePrefab.Font; public static implicit operator ScalableFont(GUIFont reference) => reference.Value; - public bool ForceUpperCase => HasValue && Value.ForceUpperCase; + public bool ForceUpperCase => Prefabs.ActivePrefab?.Font is { ForceUpperCase: true }; public uint Size => HasValue ? Value.Size : 0; private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); - private ScalableFont GetFontForStr(string str) => + public ScalableFont GetFontForStr(string str) => TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 6451c8d24..4737c85f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -161,7 +161,7 @@ namespace Barotrauma public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); public static GUIComponentStyle GetComponentStyle(string name) - => ComponentStyles.ContainsKey(name) ? ComponentStyles[name] : null; + => ComponentStyles.TryGet(name, out var style) ? style : null; public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 47ce9cab1..8ab3992d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -177,7 +177,7 @@ namespace Barotrauma radioButtonGroup = rbg; } - private void ResizeBox() + public void ResizeBox() { Vector2 textBlockScale = new Vector2(Math.Max(Rect.Width - box.Rect.Width, 0.0f) / Math.Max(Rect.Width, 1.0f), 1.0f); text.RectTransform.RelativeSize = textBlockScale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 5a750998d..03a16c643 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -233,7 +233,9 @@ namespace Barotrauma get { Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor); - Point relativeOffset = NonScaledParentRect.MultiplySize(RelativeOffset); + Point relativeOffset = new Point( + (int)(NonScaledParentSize.X * RelativeOffset.X), + (int)(NonScaledParentSize.Y * RelativeOffset.Y)); relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor); return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset; } @@ -256,6 +258,7 @@ namespace Barotrauma public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); protected virtual Rectangle NonScaledUIRect => NonScaledRect; + protected Point NonScaledParentSize => parent?.NonScaledSize ?? new Point(GUI.UIWidth, GameMain.GraphicsHeight); protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect; protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); @@ -336,6 +339,11 @@ namespace Barotrauma public event Action ChildrenChanged; public event Action ScaleChanged; public event Action SizeChanged; + + public void ResetSizeChanged() + { + SizeChanged = null; + } #endregion #region Initialization @@ -730,17 +738,17 @@ namespace Barotrauma get { return animTargetPos ?? AbsoluteOffset; } } - public void MoveOverTime(Point targetPos, float duration) + public void MoveOverTime(Point targetPos, float duration, Action onDoneMoving = null) { animTargetPos = targetPos; - CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration)); + CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration, onDoneMoving)); } public void ScaleOverTime(Point targetSize, float duration) { CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, duration)); } - private IEnumerable DoMoveAnimation(Point targetPos, float duration) + private IEnumerable DoMoveAnimation(Point targetPos, float duration, Action onDoneMoving = null) { Vector2 startPos = AbsoluteOffset.ToVector2(); float t = 0.0f; @@ -752,6 +760,7 @@ namespace Barotrauma } AbsoluteOffset = targetPos; animTargetPos = null; + onDoneMoving?.Invoke(); yield return CoroutineStatus.Success; } private IEnumerable DoScaleAnimation(Point targetSize, float duration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 6e140e57b..7a9a97fc5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -226,17 +226,18 @@ namespace Barotrauma }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); - submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Left); - submarineDisplayElement.submarineTier = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Right); - submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); - submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") + submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") { Color = Color.White, HoverColor = Color.White, PressedColor = Color.White }; + submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Left); + submarineDisplayElement.submarineTier = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Right); + submarineDisplays[i] = submarineDisplayElement; } @@ -395,9 +396,13 @@ namespace Barotrauma return true; }; - submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; + submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; + submarineDisplays[i].submarineClass.Text = TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}")); + submarineDisplays[i].submarineClass.ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}.description"); + submarineDisplays[i].submarineTier.Text = TextManager.Get($"submarinetier.{subToDisplay.Tier}"); + submarineDisplays[i].submarineTier.ToolTip = TextManager.Get("submarinetier.description"); if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 1f49c63f0..4b17d15d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1722,7 +1722,11 @@ namespace Barotrauma var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); - LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? $"{TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")} ({TextManager.Get($"submarinetier.{sub.Info.Tier}")})" : TextManager.Get("shuttle"); + LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? + TextManager.GetWithVariables("submarine.classandtier", + ("[class]", TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")), + ("[tier]", TextManager.Get($"submarinetier.{sub.Info.Tier}"))) : + TextManager.Get("shuttle"); int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; @@ -2140,18 +2144,17 @@ namespace Barotrauma skillNames.Add(skillName); skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height); skillContainer.RectTransform.MinSize = new Point(0, skillName.Rect.Height); - + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight); float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); - //TODO: if/when we upgrade to C# 9, do neater pattern matching here - string stringColor = true switch + string stringColor = skillChange switch { - true when skillChange > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), - true when skillChange < 0 => XMLExtensions.ToStringHex(GUIStyle.Red), + > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), + < 0 => XMLExtensions.ToStringHex(GUIStyle.Red), _ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal) }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index db946f01a..ad7358468 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1257,12 +1257,17 @@ namespace Barotrauma List upgrades = entity.GetUpgrades(); int upgradesCount = upgrades.Count; const int maxUpgrades = 4; - - itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); + + Item? item = entity as Item; + itemName.Text = item?.Name ?? TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); } + if (item?.PendingItemSwap != null) + { + itemName.Text = RichString.Rich(itemName.Text + "\n" + TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", item.PendingItemSwap.Name)); + } upgradeList.Content.ClearChildren(); for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) { @@ -1275,7 +1280,7 @@ namespace Barotrauma // include pending upgrades into the tooltip foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades) { - if (entity is Item item && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) + if (item != null && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) { bool found = false; foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast()) @@ -1392,6 +1397,10 @@ namespace Barotrauma if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); } + else + { + ScrollToCategory(data => data.Category.CanBeApplied(item, null)); + } } } else @@ -1463,23 +1472,16 @@ namespace Barotrauma // submarine name new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont); - GUILayoutGroup classLayout = new GUILayoutGroup(rectT(1, 0.15f, submarineInfoFrame), isHorizontal: true) { Stretch = true }; LocalizedString classText = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}"; // submarine class + tier - new GUITextBlock(rectT(0.8f, 1, classLayout), classText, textAlignment: Alignment.Right, font: GUIStyle.Font) + new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), classText, textAlignment: Alignment.Right, font: GUIStyle.Font) { - ToolTip = TextManager.Get("submarineclass.description") + '\n' + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description") + ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description") }; - int tier = submarine.Info.Tier; - string tierStyle = $"SubmarineTier.{tier}"; - if (GUIStyle.GetComponentStyle(tierStyle) != null) + new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), TextManager.Get($"submarinetier.{submarine.Info.Tier}"), textAlignment: Alignment.Right, font: GUIStyle.Font) { - LocalizedString tooltip = TextManager.Get("submarinetier.description").Fallback(string.Empty); - new GUIImage(rectT(0.15f, 1, classLayout), style: tierStyle, scaleToFit: false) - { - ToolTip = tooltip - }; - } + ToolTip = TextManager.Get("submarinetier.description") + }; var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true); submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale)); @@ -1750,14 +1752,26 @@ namespace Barotrauma { if (currentStoreLayout == null) { return; } + CategoryData? mostAppropriateCategory = null; + GUIComponent? mostAppropriateChild = null; foreach (GUIComponent child in currentStoreLayout.Content.Children) { if (child.UserData is CategoryData data && predicate(data)) { - currentStoreLayout.ScrollToElement(child, playSelectSound); - break; + //choose the category with least items in it as the "most appropriate" + //e.g. when selecting junction boxes, we want to select the "junction boxes" category instead of "electrical repairs" which contains many electrical devices + if (mostAppropriateCategory == null || + data.Category.ItemTags.Count() < mostAppropriateCategory.Value.Category.ItemTags.Count()) + { + mostAppropriateCategory = data; + mostAppropriateChild = child; + } } } + if (mostAppropriateChild != null) + { + currentStoreLayout.ScrollToElement(mostAppropriateChild, playSelectSound); + } } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index ffd792e44..226487ec1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -461,7 +461,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; - UgcTransition.Prepare(); + LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); foreach (var progress in contentPackageLoadRoutine) { @@ -715,7 +715,7 @@ namespace Barotrauma } #endif - NetworkMember?.Update((float)Timing.Step); + Client?.Update((float)Timing.Step); if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { @@ -874,7 +874,7 @@ namespace Barotrauma } } - NetworkMember?.Update((float)Timing.Step); + Client?.Update((float)Timing.Step); GUI.Update((float)Timing.Step); @@ -1181,7 +1181,7 @@ namespace Barotrauma { exiting = true; DebugConsole.NewMessage("Exiting..."); - NetworkMember?.Quit(); + Client?.Quit(); SteamManager.ShutDown(); try diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 01f09f376..a1f3594bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -3516,9 +3516,9 @@ namespace Barotrauma if (node == null || characterContext != null) { return false; } if (node.UserData is Order nodeOrder) { - return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && - (!nodeOrder.MustSetTarget || itemContext != null || - nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + return !nodeOrder.TargetAllCharacters && + (!nodeOrder.Prefab.HasOptions || !nodeOrder.Option.IsEmpty) && + (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index ea4e51c49..3dbc348f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,22 +1,21 @@ using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; namespace Barotrauma.Tutorials { enum AutoPlayVideo { Yes, No }; - enum TutorialSegmentType { MessageBox, InfoBox }; + enum TutorialSegmentType { MessageBox, InfoBox, Objective }; - class Tutorial + sealed class Tutorial { #region Constants - private const string PlayableContentPath = "Content/Tutorials/TutorialVideos/"; private const SpawnType SpawnPointType = SpawnType.Human; private const float FadeOutTime = 3f; private const float WaitBeforeFade = 4f; @@ -25,19 +24,11 @@ namespace Barotrauma.Tutorials #region Tutorial variables - public static ImmutableHashSet Types; - - static Tutorial() - { - Types = ReflectionUtils.GetDerivedNonAbstract() - .ToImmutableHashSet(); - } - public readonly Identifier Identifier; public LocalizedString DisplayName { get; } - public bool ContentRunning { get; protected set; } + public bool ContentRunning { get; private set; } private GUIComponent infoBox; private Action infoBoxClosedCallback; @@ -51,47 +42,25 @@ namespace Barotrauma.Tutorials private readonly LocalizedString objectiveTextTranslated; private readonly List ActiveObjectives = new List(); - private const float ObjectiveComponentRemovalTime = 1.5f; + private const float ObjectiveComponentAnimationTime = 1.5f; private Segment ActiveContentSegment { get; set; } public class Segment { - public struct Text + public readonly record struct Text( + Identifier Tag, + int Width = DefaultWidth, + int Height = DefaultHeight, + Anchor Anchor = Anchor.Center); + + public readonly record struct Video( + string FullPath, + Identifier TextTag, + int Width = DefaultWidth, + int Height = DefaultHeight) { - private const Anchor DefaultAnchor = Anchor.Center; - - public Identifier Tag; - public int Width; - public int Height; - public Anchor Anchor; - - public Text(Identifier tag, int? width = null, int? height = null, Anchor? anchor = null) - { - Tag = tag; - Width = width ?? DefaultWidth; - Height = height ?? DefaultHeight; - Anchor = anchor ?? DefaultAnchor; - } - - public Text(string tag, int? width = null, int? height = null, Anchor? anchor = null) : this(tag.ToIdentifier(), width, height, anchor) { } - } - - public struct Video - { - public string File; - public Identifier TextTag; - public int Width; - public int Height; - - public Video(string file, Identifier textTag, int? width = null, int? height = null) - { - File = file; - TextTag = textTag; - Width = width ?? DefaultWidth; - Height = height ?? DefaultHeight; - } - - public Video(string file, string textTag, int? width = null, int? height = null) : this(file, textTag.ToIdentifier(), width, height) { } + public string FileName => Path.GetFileName(FullPath.CleanUpPath()); + public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); } private const int DefaultWidth = 450; @@ -103,15 +72,30 @@ namespace Barotrauma.Tutorials public LocalizedString ObjectiveText; public readonly Identifier Id; - public readonly Text? TextContent; - public readonly Video? VideoContent; + public readonly Text TextContent; + public readonly Video VideoContent; public readonly AutoPlayVideo AutoPlayVideo; - public Action OnClickToDisplayMessage; + public Action OnClickObjective; public readonly TutorialSegmentType SegmentType; - public Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text? textContent = null, Video? videoContent = null) + public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); + } + + public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + return new Segment(id, objectiveTextTag, onClickObjective); + } + + public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) + { + return new Segment(id, objectiveTextTag); + } + + private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { Id = id; ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); @@ -121,15 +105,21 @@ namespace Barotrauma.Tutorials SegmentType = TutorialSegmentType.InfoBox; } - public Segment(Identifier id, Action onClickToDisplayMessage) + private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) { Id = id; - var objetiveTextTag = $"{id}.objective".ToIdentifier(); - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objetiveTextTag)); - OnClickToDisplayMessage = onClickToDisplayMessage; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + OnClickObjective = onClickObjective; SegmentType = TutorialSegmentType.MessageBox; } - } + + private Segment(Identifier id, Identifier objectiveTextTag) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + SegmentType = TutorialSegmentType.Objective; + } + } private bool completed; public bool Completed @@ -157,60 +147,57 @@ namespace Barotrauma.Tutorials private Character character; - private readonly string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub"; - private readonly string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub"; - - private readonly string levelSeed = "nLoZLLtza"; - private readonly string levelParams = "ColdCavernsTutorial"; + private string SubmarinePath => TutorialPrefab.SubmarinePath.Value; + private string StartOutpostPath => TutorialPrefab.OutpostPath.Value; + private string LevelSeed => TutorialPrefab.LevelSeed; + private string LevelParams => TutorialPrefab.LevelParams; private SubmarineInfo startOutpost = null; + public readonly List<(Entity entity, string iconStyle)> Icons = new List<(Entity entity, string iconStyle)>(); + #endregion #region Tutorial Controls public Tutorial(TutorialPrefab prefab) { - Identifier = $"tutorial.{prefab?.Identifier ?? Identifier.Empty}".ToIdentifier(); + Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier(); DisplayName = TextManager.Get(Identifier); objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); TutorialPrefab = prefab; - submarinePath = prefab.SubmarinePath.Value; - startOutpostPath = prefab.OutpostPath.Value; - levelSeed = prefab.LevelSeed; - levelParams = prefab.LevelParams; eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); } private IEnumerable Loading() { - SubmarineInfo subInfo = new SubmarineInfo(submarinePath); + SubmarineInfo subInfo = new SubmarineInfo(SubmarinePath); - LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier == levelParams); + LevelGenerationParams.LevelParams.TryGet(LevelParams, out LevelGenerationParams generationParams); yield return CoroutineStatus.Running; GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; - if (generationParams != null) + if (generationParams is not null) { Biome biome = Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? Biome.Prefabs.First(); - if (!string.IsNullOrEmpty(startOutpostPath)) + if (!string.IsNullOrEmpty(StartOutpostPath)) { - startOutpost = new SubmarineInfo(startOutpostPath); + startOutpost = new SubmarineInfo(StartOutpostPath); } - LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome); + LevelData tutorialLevel = new LevelData(LevelSeed, 0, 0, generationParams, biome); GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost); } else { - GameMain.GameSession.StartRound(levelSeed); + GameMain.GameSession.StartRound(LevelSeed); } GameMain.GameSession.EventManager.ActiveEvents.Clear(); @@ -274,6 +261,7 @@ namespace Barotrauma.Tutorials private void Initialize() { GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; + GameMain.GameSession.CrewManager.AutoHideCrewList(); if (Character.Controlled is Character character) { @@ -450,7 +438,7 @@ namespace Barotrauma.Tutorials public void TriggerTutorialSegment(Segment segment) { - if (segment.SegmentType == TutorialSegmentType.MessageBox) + if (segment.SegmentType != TutorialSegmentType.InfoBox) { ActiveObjectives.Add(segment); AddToObjectiveList(segment); @@ -462,7 +450,7 @@ namespace Barotrauma.Tutorials ActiveContentSegment = segment; var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Value.Tag); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); tutorialText = TextManager.ParseInputTypes(tutorialText); switch (segment.AutoPlayVideo) @@ -471,9 +459,9 @@ namespace Barotrauma.Tutorials infoBox = CreateInfoFrame( title, tutorialText, - segment.TextContent.Value.Width, - segment.TextContent.Value.Height, - segment.TextContent.Value.Anchor, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, hasButton: true, onInfoBoxClosed: LoadActiveContentVideo); break; @@ -481,9 +469,9 @@ namespace Barotrauma.Tutorials infoBox = CreateInfoFrame( title, tutorialText, - segment.TextContent.Value.Width, - segment.TextContent.Value.Height, - segment.TextContent.Value.Anchor, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, hasButton: true, onInfoBoxClosed: StopCurrentContentSegment, onVideoButtonClicked: LoadActiveContentVideo); @@ -493,7 +481,7 @@ namespace Barotrauma.Tutorials public void CompleteTutorialSegment(Identifier segmentId) { - if (!(GetActiveObjective(segmentId) is Segment segment)) + if (GetActiveObjective(segmentId) is not Segment segment) { DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); return; @@ -501,29 +489,28 @@ namespace Barotrauma.Tutorials if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) { segment.ObjectiveStateIndicator.ApplyStyle(style); - segment.ObjectiveStateIndicator.Flash(color: GUIStyle.Green); } + segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); segment.ObjectiveButton.OnClicked = null; segment.ObjectiveButton.CanBeFocused = false; } public void RemoveTutorialSegment(Identifier segmentId) { - if (!(GetActiveObjective(segmentId) is Segment segment)) + if (GetActiveObjective(segmentId) is not Segment segment) { DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); return; } - segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentRemovalTime, false); - segment.LinkedTextBlock.FadeOut(ObjectiveComponentRemovalTime, false); + segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); + segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); var parent = segment.LinkedTextBlock.Parent; - parent.FadeOut(ObjectiveComponentRemovalTime, true, onRemove: () => + parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => { ActiveObjectives.Remove(segment); objectiveGroup?.Recalculate(); }); - var targetPos = new Point(GameMain.GraphicsWidth - parent.Rect.X, 0); - parent.RectTransform.MoveOverTime(targetPos, ObjectiveComponentRemovalTime); + parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); segment.ObjectiveButton.OnClicked = null; segment.ObjectiveButton.CanBeFocused = false; } @@ -585,12 +572,14 @@ namespace Barotrauma.Tutorials { var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) { + AbsoluteOffset = GetObjectiveHiddenPosition(), MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) }; var frame = new GUIFrame(frameRt, style: null) { CanBeFocused = true }; + objectiveGroup.Recalculate(); segment.LinkedTextBlock = new GUITextBlock( new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), @@ -612,6 +601,7 @@ namespace Barotrauma.Tutorials segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) { + CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective, ToolTip = objectiveTextTranslated, OnClicked = (GUIButton btn, object userdata) => { @@ -628,13 +618,15 @@ namespace Barotrauma.Tutorials } else if (segment.SegmentType == TutorialSegmentType.MessageBox) { - segment.OnClickToDisplayMessage?.Invoke(); + segment.OnClickObjective?.Invoke(); } return true; } }; SetTransparent(segment.ObjectiveButton); + frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); + static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; } @@ -654,15 +646,20 @@ namespace Barotrauma.Tutorials ActiveContentSegment = segment; infoBox = CreateInfoFrame( TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Value.Tag), - segment.TextContent.Value.Width, - segment.TextContent.Value.Height, - segment.TextContent.Value.Anchor, + TextManager.Get(segment.TextContent.Tag), + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, hasButton: true, onInfoBoxClosed: () => ContentRunning = false, onVideoButtonClicked: () => LoadVideo(segment)); } + private Point GetObjectiveHiddenPosition(RectTransform rt = null) + { + return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); + } + #endregion #region InfoFrame @@ -772,9 +769,9 @@ namespace Barotrauma.Tutorials if (segment.AutoPlayVideo == AutoPlayVideo.Yes) { videoPlayer.LoadContent( - contentPath: PlayableContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), - textSettings: new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width), + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), contentId: segment.Id, startPlayback: true, objective: segment.ObjectiveText, @@ -783,8 +780,8 @@ namespace Barotrauma.Tutorials else { videoPlayer.LoadContent( - contentPath: PlayableContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), textSettings: null, contentId: segment.Id, startPlayback: true, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 39982b6cd..8a605499d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; if (GUI.HideCursor) { - crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale); + crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index a1b1857e8..77e267d14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -369,6 +369,7 @@ namespace Barotrauma.Items.Components loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(position.X, position.Y, 0.0f), 0.01f, + freqMult: itemSound.RoundSound.GetRandomFrequencyMultiplier(), muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; //TODO: tweak diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 2deca5a59..dbac05552 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1013,6 +1013,19 @@ namespace Barotrauma.Items.Components if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } + + bool disrupted = false; + foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections) + { + float dot = Vector2.Dot(Vector2.Normalize(c.center - transducerCenter), disruptPos); + if (dot > 1.0f - disruptStrength) + { + disrupted = true; + break; + } + } + if (disrupted) { continue; } + DrawMarker(spriteBatch, i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 83f660f0d..a6c9f47da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components private GUILayoutGroup extraButtonContainer; + private GUIComponent skillTextContainer; + private readonly List particleEmitters = new List(); //the corresponding particle emitter is active when the condition is within this range private readonly List particleEmitterConditionRanges = new List(); @@ -132,9 +134,10 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); + skillTextContainer = paddedFrame; for (int i = 0; i < requiredSkills.Count; i++) { - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillTextContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), font: GUIStyle.SmallFont) { @@ -359,19 +362,11 @@ namespace Barotrauma.Items.Components extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible; extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.Visible; - foreach (GUIComponent c in GuiFrame.GetChild(0).Children) + foreach (GUIComponent c in skillTextContainer.Children) { - if (!(c.UserData is Skill skill)) continue; - + if (c.UserData is not Skill skill) { continue; } GUITextBlock textBlock = (GUITextBlock)c; - if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) - { - textBlock.TextColor = GUIStyle.Red; - } - else - { - textBlock.TextColor = Color.White; - } + textBlock.TextColor = character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier) ? GUIStyle.Red : GUIStyle.TextColorNormal; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 1a6590180..8950850cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -823,7 +823,7 @@ namespace Barotrauma reloadTextureButton.OnClicked += (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(updateAllSprites: true); + Sprite.ReloadTexture(); return true; }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 1342557f5..b41a0f0a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -163,7 +163,7 @@ namespace Barotrauma OnClicked = (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(updateAllSprites: true); + Sprite.ReloadTexture(); return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index f0047e10c..a08893601 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -93,10 +93,11 @@ namespace Barotrauma { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; - LocalizedString className = !HasTag(SubmarineTag.Shuttle) - ? $"{TextManager.Get($"submarineclass.{SubmarineClass}")} ({TextManager.Get($"submarinetier.{Tier}")})" - : TextManager.Get("shuttle"); - + LocalizedString className = !HasTag(SubmarineTag.Shuttle) ? + TextManager.GetWithVariables("submarine.classandtier", + ("[class]", TextManager.Get($"submarineclass.{SubmarineClass}")), + ("[tier]", TextManager.Get($"submarinetier.{Tier}"))) : + TextManager.Get("shuttle"); int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 480fa90cc..3c72ee33a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -134,6 +134,14 @@ namespace Barotrauma.Networking return Permissions.HasFlag(permission); } + public void ResetVotes() + { + for (int i = 0; i < votes.Length; i++) + { + votes[i] = null; + } + } + partial void DisposeProjSpecific() { if (VoipQueue != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 30ab2172f..c78321411 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -14,12 +14,12 @@ using System.Xml.Linq; namespace Barotrauma.Networking { - class GameClient : NetworkMember + sealed class GameClient : NetworkMember { - public override bool IsClient - { - get { return true; } - } + public override bool IsClient => true; + public override bool IsServer => false; + + public override Voting Voting { get; } private UInt16 nameId = 0; @@ -29,7 +29,6 @@ namespace Barotrauma.Networking public void SetName(string value) { - value = value.Replace(":", "").Replace(";", ""); if (string.IsNullOrEmpty(value)) { return; } Name = value; nameId++; @@ -40,8 +39,7 @@ namespace Barotrauma.Networking nameId++; } - private ClientPeer clientPeer; - public ClientPeer ClientPeer { get { return clientPeer; } } + public ClientPeer ClientPeer { get; private set; } private GUIMessageBox reconnectBox, waitInServerQueueBox; @@ -56,7 +54,7 @@ namespace Barotrauma.Networking public GUITickBox FollowSubTickBox => cameraFollowsSub; public bool IsFollowSubTickBoxVisible => - gameStarted && Screen.Selected == GameMain.GameScreen && + GameStarted && Screen.Selected == GameMain.GameScreen && cameraFollowsSub != null && cameraFollowsSub.Visible; public CameraTransition EndCinematic; @@ -88,9 +86,6 @@ namespace Barotrauma.Networking public string ServerName { get; private set; } - private bool allowReconnect; - private bool requiresPw; - private int pwRetries; private bool canStart; private UInt16 lastSentChatMsgID = 0; //last message this client has successfully sent @@ -99,14 +94,10 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID; - private readonly ClientEntityEventManager entityEventManager; - - private readonly FileReceiver fileReceiver; - #if DEBUG public void PrintReceiverTransters() { - foreach (var transfer in fileReceiver.ActiveTransfers) + foreach (var transfer in FileReceiver.ActiveTransfers) { DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); } @@ -136,26 +127,30 @@ namespace Barotrauma.Networking } } + public Option Ping + { + get + { + Client selfClient = ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId); + if (selfClient is null || selfClient.Ping == 0) { return Option.None(); } + return Option.Some(selfClient.Ping); + } + } + private readonly List previouslyConnectedClients = new List(); public IEnumerable PreviouslyConnectedClients { get { return previouslyConnectedClients; } } - public FileReceiver FileReceiver - { - get { return fileReceiver; } - } + public readonly FileReceiver FileReceiver; public bool MidRoundSyncing { - get { return entityEventManager.MidRoundSyncing; } + get { return EntityEventManager.MidRoundSyncing; } } - public ClientEntityEventManager EntityEventManager - { - get { return entityEventManager; } - } + public readonly ClientEntityEventManager EntityEventManager; public bool? WaitForNextRoundRespawn { @@ -189,8 +184,6 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.NotStarted; - allowReconnect = true; - NetStats = new NetStats(); inGameHUD = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) @@ -237,13 +230,13 @@ namespace Barotrauma.Networking { OnClicked = (GUIButton button, object userData) => { - if (serverSettings.ServerLog.LogFrame == null) + if (ServerSettings.ServerLog.LogFrame == null) { - serverSettings.ServerLog.CreateLogFrame(); + ServerSettings.ServerLog.CreateLogFrame(); } else { - serverSettings.ServerLog.LogFrame = null; + ServerSettings.ServerLog.LogFrame = null; GUI.KeyboardDispatcher.Subscriber = null; } return true; @@ -257,11 +250,11 @@ namespace Barotrauma.Networking SetName(newName); - entityEventManager = new ClientEntityEventManager(this); + EntityEventManager = new ClientEntityEventManager(this); - fileReceiver = new FileReceiver(); - fileReceiver.OnFinished += OnFileReceived; - fileReceiver.OnTransferFailed += OnTransferFailed; + FileReceiver = new FileReceiver(); + FileReceiver.OnFinished += OnFileReceived; + FileReceiver.OnTransferFailed += OnTransferFailed; characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, Name, originalName: null) { @@ -270,7 +263,7 @@ namespace Barotrauma.Networking otherClients = new List(); - serverSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); + ServerSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); Voting = new Voting(); serverEndpoint = endpoint; @@ -282,6 +275,13 @@ namespace Barotrauma.Networking GameMain.ResetNetLobbyScreen(); } + public ServerInfo CreateServerInfoFromSettings() + { + var serverInfo = ServerInfo.FromServerConnection(ClientPeer.ServerConnection, ServerSettings); + GameMain.ServerListScreen.UpdateOrAddServerInfo(serverInfo); + return serverInfo; + } + private void InitiateServerJoin(string hostName) { LastClientListUpdateID = 0; @@ -304,11 +304,9 @@ namespace Barotrauma.Networking myCharacter = Character.Controlled; ChatMessage.LastID = 0; - clientPeer?.Close(); - clientPeer = CreateNetPeer(); - clientPeer.Start(); - - updateInterval = new TimeSpan(0, 0, 0, 0, 150); + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + ClientPeer = CreateNetPeer(); + ClientPeer.Start(); CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } @@ -318,16 +316,6 @@ namespace Barotrauma.Networking 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 { @@ -362,21 +350,15 @@ namespace Barotrauma.Networking private bool connectCancelled; private void CancelConnect() { - ChildServerRelay.ShutDown(); - connectCancelled = true; Quit(); } - private bool wrongPassword; - // Before main looping starts, we loop here and wait for approval message private IEnumerable WaitForStartingInfo() { GUI.SetCursorWaiting(); - requiresPw = false; - pwRetries = -1; - connectCancelled = wrongPassword = false; + connectCancelled = false; // When this is set to true, we are approved and ready to go canStart = false; @@ -390,7 +372,7 @@ namespace Barotrauma.Networking if (reconnectBox == null && waitInServerQueueBox == null) { string serverDisplayName = ServerName; - if (string.IsNullOrEmpty(serverDisplayName) && clientPeer?.ServerConnection is SteamP2PConnection steamConnection) + if (string.IsNullOrEmpty(serverDisplayName) && ClientPeer?.ServerConnection is SteamP2PConnection steamConnection) { if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) { @@ -404,12 +386,9 @@ namespace Barotrauma.Networking } if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown").Value; } - reconnectBox = new GUIMessageBox( + CreateReconnectBox( connectingText, - TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName), - new LocalizedString[] { TextManager.Get("Cancel") }); - reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; - reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; + TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName)); } if (reconnectBox != null) @@ -421,82 +400,29 @@ namespace Barotrauma.Networking if (DateTime.Now > timeOut) { - clientPeer?.Close(Lidgren.Network.NetConnection.NoResponseMessage); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer")); + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer")) + { + DisplayInLoadingScreens = true + }; msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); break; } - if (requiresPw && !canStart && !connectCancelled) + if (ClientPeer.WaitingForPassword && !canStart && !connectCancelled) { GUI.ClearCursorWait(); - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); - LocalizedString pwMsg = TextManager.Get("PasswordRequired"); - - var msgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); - var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); - var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) + while (ClientPeer.WaitingForPassword) { - UserData = "password", - Censor = true - }; - - if (wrongPassword) - { - var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); - incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); - passwordHolder.Recalculate(); - } - - msgBox.Content.Recalculate(); - msgBox.Content.RectTransform.MinSize = new Point(0, msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); - msgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(msgBox.Content.RectTransform.MinSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); - - var okButton = msgBox.Buttons[0]; - okButton.OnClicked += msgBox.Close; - var cancelButton = msgBox.Buttons[1]; - cancelButton.OnClicked += msgBox.Close; - passwordBox.OnEnterPressed += (GUITextBox textBox, string text) => - { - msgBox.Close(); - clientPeer?.SendPassword(passwordBox.Text); - requiresPw = false; - return true; - }; - - okButton.OnClicked += (GUIButton button, object obj) => - { - clientPeer?.SendPassword(passwordBox.Text); - requiresPw = false; - return true; - }; - - cancelButton.OnClicked += (GUIButton button, object obj) => - { - requiresPw = false; - connectCancelled = true; - GameMain.ServerListScreen.Select(); - return true; - }; - yield return CoroutineStatus.Running; - passwordBox.Select(); - - while (GUIMessageBox.MessageBoxes.Contains(msgBox)) - { - if (!requiresPw) - { - msgBox.Close(); - break; - } yield return CoroutineStatus.Running; } } } - reconnectBox?.Close(); reconnectBox = null; + CloseReconnectBox(); GUI.ClearCursorWait(); if (connectCancelled) { yield return CoroutineStatus.Success; } @@ -504,7 +430,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public override void Update(float deltaTime) + public void Update(float deltaTime) { #if DEBUG if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.P)) return; @@ -541,8 +467,6 @@ namespace Barotrauma.Networking UpdateHUD(deltaTime); - base.Update(deltaTime); - try { incomingMessagesToProcess.Clear(); @@ -552,7 +476,7 @@ namespace Barotrauma.Networking ReadDataMessage(inc); } pendingIncomingMessages.Clear(); - clientPeer?.Update(deltaTime); + ClientPeer?.Update(deltaTime); } catch (Exception e) { @@ -571,19 +495,15 @@ namespace Barotrauma.Networking return; } - if (!connected) return; + if (!connected) { return; } - if (reconnectBox != null) - { - reconnectBox.Close(); - reconnectBox = null; - } + CloseReconnectBox(); - if (gameStarted && Screen.Selected == GameMain.GameScreen) + if (GameStarted && Screen.Selected == GameMain.GameScreen) { EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); - respawnManager?.Update(deltaTime); + RespawnManager?.Update(deltaTime); if (updateTimer <= DateTime.Now) { @@ -598,7 +518,7 @@ namespace Barotrauma.Networking } } - if (serverSettings.VoiceChatEnabled) + if (ServerSettings.VoiceChatEnabled) { VoipClient?.SendToServer(); } @@ -622,7 +542,7 @@ namespace Barotrauma.Networking if (updateTimer <= DateTime.Now) { // Update current time - updateTimer = DateTime.Now + updateInterval; + updateTimer = DateTime.Now + UpdateInterval; } } @@ -633,11 +553,12 @@ namespace Barotrauma.Networking { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize && - roundInitStatus == RoundInitStatus.Started && - header != ServerPacketHeader.ENDGAME && - header != ServerPacketHeader.PING_REQUEST && - header != ServerPacketHeader.FILE_TRANSFER) + if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize + && header is not ( + ServerPacketHeader.STARTGAMEFINALIZE + or ServerPacketHeader.ENDGAME + or ServerPacketHeader.PING_REQUEST + or ServerPacketHeader.FILE_TRANSFER)) { //rewind the header byte we just read inc.BitPosition -= 8; @@ -660,10 +581,10 @@ namespace Barotrauma.Networking //allow interpreting this packet break; case ServerPacketHeader.STARTGAME: - gameStarted = true; + GameStarted = true; return; case ServerPacketHeader.ENDGAME: - gameStarted = false; + GameStarted = false; return; default: return; //ignore any other packets @@ -682,7 +603,7 @@ namespace Barotrauma.Networking byte b = inc.ReadByte(); response.WriteByte(b); } - clientPeer.Send(response, DeliveryMethod.Unreliable); + ClientPeer.Send(response, DeliveryMethod.Unreliable); break; case ServerPacketHeader.CLIENT_PINGS: byte clientCount = inc.ReadByte(); @@ -776,7 +697,7 @@ namespace Barotrauma.Networking WriteCharacterInfo(readyToStartMsg); - clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); + ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { @@ -803,7 +724,7 @@ namespace Barotrauma.Networking //waiting for a save file if (campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) && - fileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) + FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) { return; } @@ -898,7 +819,7 @@ namespace Barotrauma.Networking ReadyCheck.ClientRead(inc); break; case ServerPacketHeader.FILE_TRANSFER: - fileReceiver.ReadMessage(inc); + FileReceiver.ReadMessage(inc); break; case ServerPacketHeader.TRAITOR_MESSAGE: ReadTraitorMessage(inc); @@ -1003,204 +924,136 @@ namespace Barotrauma.Networking /// /// 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) + private void OnClientPeerDisconnect(PeerDisconnectPacket disconnectPacket) { + bool wasConnected = connected; + connected = false; + connectCancelled = true; + CoroutineManager.StopCoroutines("WaitForStartingInfo"); - reconnectBox?.Close(); - reconnectBox = null; + CloseReconnectBox(); GUI.ClearCursorWait(); - if (disableReconnect) { allowReconnect = false; } - if (!this.allowReconnect) { CancelConnect(); } - + ChildServerRelay.ShutDown(); + if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); } - } - private void HandleDisconnectMessage(string disconnectMsg) - { - disconnectMsg = disconnectMsg ?? ""; - - string[] splitMsg = disconnectMsg.Split('/'); - DisconnectReason disconnectReason = DisconnectReason.Unknown; - bool disconnectReasonIncluded = false; - if (splitMsg.Length > 0) - { - if (Enum.TryParse(splitMsg[0], out disconnectReason)) { disconnectReasonIncluded = true; } - } - - if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage || - disconnectReason == DisconnectReason.Banned || - disconnectReason == DisconnectReason.Kicked || - disconnectReason == DisconnectReason.TooManyFailedLogins) - { - allowReconnect = false; - } - - DebugConsole.NewMessage("Received a disconnect message (" + disconnectMsg + ")"); - - if (disconnectReason != DisconnectReason.Banned && - disconnectReason != DisconnectReason.ServerShutdown && - disconnectReason != DisconnectReason.TooManyFailedLogins && - disconnectReason != DisconnectReason.MissingContentPackage && - disconnectReason != DisconnectReason.InvalidVersion) + if (disconnectPacket.ShouldCreateAnalyticsEvent) { GameAnalyticsManager.AddErrorEventOnce( "GameClient.HandleDisconnectMessage", GameAnalyticsManager.ErrorSeverity.Debug, - "Client received a disconnect message. Reason: " + disconnectReason.ToString()); + $"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}"); } - - if (disconnectReason == DisconnectReason.ServerFull) + + if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull) { - CoroutineManager.StopCoroutines("WaitForStartingInfo"); - //already waiting for a slot to free up, stop waiting for starting info and - //let WaitInServerQueue reattempt connecting later - if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) - { - return; - } - - reconnectBox?.Close(); reconnectBox = null; - - var queueBox = new GUIMessageBox( - TextManager.Get("DisconnectReason.ServerFull"), - TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); - - queueBox.Buttons[0].OnClicked += queueBox.Close; - queueBox.Buttons[1].OnClicked += queueBox.Close; - queueBox.Buttons[1].OnClicked += (btn, userdata) => - { - reconnectBox?.Close(); reconnectBox = null; - CoroutineManager.StartCoroutine(WaitInServerQueue(), "WaitInServerQueue"); - return true; - }; - return; + AskToWaitInQueue(); } - else + else if (disconnectPacket.ShouldAttemptReconnect && !IsServerOwner && wasConnected) { - //disconnected/denied for some other reason than the server being full - // -> stop queuing and show a message box - waitInServerQueueBox?.Close(); - waitInServerQueueBox = null; - CoroutineManager.StopCoroutines("WaitInServerQueue"); - } - - bool eventSyncError = - disconnectReason == DisconnectReason.ExcessiveDesyncOldEvent || - disconnectReason == DisconnectReason.ExcessiveDesyncRemovedEvent || - disconnectReason == DisconnectReason.SyncTimeout; - - if (allowReconnect && - (disconnectReason == DisconnectReason.Unknown || eventSyncError)) - { - if (eventSyncError) + if (disconnectPacket.IsEventSyncError) { GameMain.NetLobbyScreen.Select(); GameMain.GameSession?.EndRound("", null); - gameStarted = false; + GameStarted = false; myCharacter = null; } - - DebugConsole.NewMessage("Attempting to reconnect..."); - - //if the first part of the message is the disconnect reason Enum, don't include it in the popup message - LocalizedString msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); - msg = msg.IsNullOrWhiteSpace() ? - TextManager.Get("ConnectionLostReconnecting") : - msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); - - reconnectBox?.Close(); - reconnectBox = new GUIMessageBox( - TextManager.Get("ConnectionLost"), msg, - new LocalizedString[] { TextManager.Get("Cancel") }) + AttemptReconnect(disconnectPacket); + } + else if (disconnectPacket.ShouldShowMessage) + { + ReturnToPreviousMenu(null, null); + var msgBox = new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) { 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--; - InitiateServerJoin(ServerName); - if (clientPeer != null) - { - //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match - clientPeer.ServerContentPackages = prevContentPackages; - } - } - else - { - connected = false; - connectCancelled = true; - - LocalizedString msg = ""; - if (disconnectReason == DisconnectReason.Unknown) - { - DebugConsole.NewMessage("Not attempting to reconnect (unknown disconnect reason)."); - msg = disconnectMsg; - } - else - { - DebugConsole.NewMessage("Not attempting to reconnect (DisconnectReason doesn't allow reconnection)."); - msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()) + " "; - - for (int i = 1; i < splitMsg.Length; i++) - { - msg += TextManager.GetServerMessage(splitMsg[i]); - } - - if (disconnectReason == DisconnectReason.ServerCrashed && IsServerOwner) - { - msg = TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", ChildServerRelay.CrashReportFilePath); - } - } - - reconnectBox?.Close(); - - if (msg == Lidgren.Network.NetConnection.NoResponseMessage) - { - //display a generic "could not connect" popup if the message is Lidgren's "failed to establish connection" - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer")) - { - DisplayInLoadingScreens = true - }; - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } - else - { - var msgBox = new GUIMessageBox(TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer"), msg) - { - DisplayInLoadingScreens = true - }; - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } - - if (disconnectReason == DisconnectReason.InvalidName) - { - GameMain.ServerListScreen.ClientNameBox.Text = ""; - GameMain.ServerListScreen.ClientNameBox.Flash(flashDuration: 5.0f); - GameMain.ServerListScreen.ClientNameBox.Select(); - } } } + private void CreateReconnectBox(LocalizedString headerText, LocalizedString bodyText) + { + reconnectBox = new GUIMessageBox( + headerText, + bodyText, + new LocalizedString[] { TextManager.Get("Cancel") }) + { + DisplayInLoadingScreens = true + }; + reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; + reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; + } + + private void CloseReconnectBox() + { + reconnectBox?.Close(); + reconnectBox = null; + } + + private void AskToWaitInQueue() + { + CoroutineManager.StopCoroutines("WaitForStartingInfo"); + //already waiting for a slot to free up, stop waiting for starting info and + //let WaitInServerQueue reattempt connecting later + if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) + { + return; + } + + var queueBox = new GUIMessageBox( + TextManager.Get("DisconnectReason.ServerFull"), + TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); + + queueBox.Buttons[0].OnClicked += queueBox.Close; + queueBox.Buttons[1].OnClicked += queueBox.Close; + queueBox.Buttons[1].OnClicked += (btn, userdata) => + { + CloseReconnectBox(); + CoroutineManager.StartCoroutine(WaitInServerQueue(), "WaitInServerQueue"); + return true; + }; + } + + private void AttemptReconnect(PeerDisconnectPacket peerDisconnectPacket) + { + connectCancelled = false; + + CreateReconnectBox( + TextManager.Get("ConnectionLost"), + peerDisconnectPacket.ReconnectMessage); + + var prevContentPackages = ClientPeer.ServerContentPackages; + //decrement lobby update ID to make sure we update the lobby when we reconnect + GameMain.NetLobbyScreen.LastUpdateID--; + InitiateServerJoin(ServerName); + if (ClientPeer != null) + { + //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match + ClientPeer.ServerContentPackages = prevContentPackages; + } + } + private void OnConnectionInitializationComplete() { if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + ServerName); - Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + ServerName.Replace("\"", "\\\"") + "\" " + serverEndpoint); + Steamworks.SteamFriends.SetRichPresence("servername", ServerName); + #warning TODO: use Steamworks localization functionality + Steamworks.SteamFriends.SetRichPresence("status", + TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName).Value); + Steamworks.SteamFriends.SetRichPresence("connect", + $"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {serverEndpoint}"); } canStart = true; connected = true; - VoipClient = new VoipClient(this, clientPeer); + VoipClient = new VoipClient(this, ClientPeer); //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods if (!(Screen.Selected is GameScreen) && !(Screen.Selected is RoundSummaryScreen) && !(Screen.Selected is NetLobbyScreen)) @@ -1209,7 +1062,7 @@ namespace Barotrauma.Networking } else { - entityEventManager.ClearSelf(); + EntityEventManager.ClearSelf(); foreach (Character c in Character.CharacterList) { c.ResetNetState(); @@ -1433,7 +1286,7 @@ namespace Barotrauma.Networking //(for example, due to a missing sub file or an error) GameMain.NetLobbyScreen.ShowSpectateButton(); - entityEventManager.Clear(); + EntityEventManager.Clear(); LastSentEntityEventID = 0; EndVoteTickBox.Selected = false; @@ -1454,19 +1307,19 @@ namespace Barotrauma.Networking } bool respawnAllowed = inc.ReadBoolean(); - serverSettings.AllowDisguises = inc.ReadBoolean(); - serverSettings.AllowRewiring = inc.ReadBoolean(); - serverSettings.AllowFriendlyFire = inc.ReadBoolean(); - serverSettings.LockAllDefaultWires = inc.ReadBoolean(); - serverSettings.AllowRagdollButton = inc.ReadBoolean(); - serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); - serverSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); + ServerSettings.AllowDisguises = inc.ReadBoolean(); + ServerSettings.AllowRewiring = inc.ReadBoolean(); + ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); + ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); + ServerSettings.AllowRagdollButton = inc.ReadBoolean(); + ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); + ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; - serverSettings.ReadMonsterEnabled(inc); + ServerSettings.ReadMonsterEnabled(inc); Rand.SetSyncedSeed(seed); @@ -1522,7 +1375,7 @@ namespace Barotrauma.Networking errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation + " != " + subHash; } } - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -1533,7 +1386,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName || GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash) { - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ")."; DebugConsole.ThrowError(errorMsg); @@ -1565,7 +1418,7 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (campaign ID does not match)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1573,7 +1426,7 @@ namespace Barotrauma.Networking } else if (campaign.Map == null) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1588,7 +1441,7 @@ namespace Barotrauma.Networking { if (DateTime.Now > saveFileTimeOut) { - gameStarted = true; + GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; @@ -1626,7 +1479,7 @@ namespace Barotrauma.Networking } } - if (clientPeer == null) + if (ClientPeer == null) { DebugConsole.ThrowError("There was an error initializing the round (disconnected during the StartGame coroutine.)"); roundInitStatus = RoundInitStatus.Error; @@ -1641,7 +1494,7 @@ namespace Barotrauma.Networking TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); - clientPeer.Send(msg, DeliveryMethod.Unreliable); + ClientPeer.Send(msg, DeliveryMethod.Unreliable); GUIMessageBox interruptPrompt = null; @@ -1655,7 +1508,7 @@ namespace Barotrauma.Networking { msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); - clientPeer.Send(msg, DeliveryMethod.Unreliable); + ClientPeer.Send(msg, DeliveryMethod.Unreliable); requestFinalizeTime = DateTime.Now + requestFinalizeInterval; } if (DateTime.Now > timeOut && interruptPrompt == null) @@ -1669,7 +1522,7 @@ namespace Barotrauma.Networking { roundInitStatus = RoundInitStatus.Interrupted; DebugConsole.ThrowError("Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Returning to the lobby..."); - gameStarted = true; + GameStarted = true; GameMain.NetLobbyScreen.Select(); interruptPrompt.Close(); interruptPrompt = null; @@ -1762,10 +1615,10 @@ namespace Barotrauma.Networking if (respawnAllowed) { - respawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); + RespawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); } - gameStarted = true; + GameStarted = true; ServerSettings.ServerDetailsChanged = true; if (roundSummary != null) @@ -1797,7 +1650,7 @@ namespace Barotrauma.Networking yield return new WaitForSeconds(1.0f); } - if (!gameStarted) + if (!GameStarted) { GameMain.NetLobbyScreen.Select(); yield return CoroutineStatus.Success; @@ -1807,13 +1660,13 @@ namespace Barotrauma.Networking ServerSettings.ServerDetailsChanged = true; - gameStarted = false; + GameStarted = false; Character.Controlled = null; WaitForNextRoundRespawn = null; SpawnAsTraitor = false; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - respawnManager = null; + RespawnManager = null; if (Screen.Selected == GameMain.GameScreen) { @@ -1885,12 +1738,12 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, ServerSubmarines); GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, ServerSubmarines.Where(s => s.HasTag(SubmarineTag.Shuttle))); - gameStarted = inc.ReadBoolean(); + GameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); ReadPermissions(inc); - if (gameStarted) + if (GameStarted) { if (Screen.Selected != GameMain.GameScreen) { @@ -1994,15 +1847,15 @@ namespace Barotrauma.Networking } if (updateClientListId) { LastClientListUpdateID = listId; } - if (clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2POwnerPeer) { TaskPool.Add("WaitForPingDataAsync (owner)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => { - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); }); - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); } } @@ -2083,10 +1936,10 @@ namespace Barotrauma.Networking { ReadWriteMessage settingsBuf = new ReadWriteMessage(); settingsBuf.WriteBytes(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0; - serverSettings.ClientRead(settingsBuf); + ServerSettings.ClientRead(settingsBuf); if (!IsServerOwner) { - ServerInfo info = serverSettings.GetServerListInfo(); + ServerInfo info = CreateServerInfoFromSettings(); GameMain.ServerListScreen.AddToRecentServers(info); GameMain.NetLobbyScreen.Favorite.Visible = true; GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info); @@ -2098,10 +1951,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.LastUpdateID = updateID; - serverSettings.ServerLog.ServerName = serverSettings.ServerName; + ServerSettings.ServerLog.ServerName = ServerSettings.ServerName; - if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = serverSettings.ServerName; } - if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = serverSettings.ServerMessageText; } + if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = ServerSettings.ServerName; } + if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = ServerSettings.ServerMessageText; } GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } @@ -2131,13 +1984,13 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(botCount); GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); - serverSettings.VoiceChatEnabled = voiceChatEnabled; - serverSettings.AllowSubVoting = allowSubVoting; - serverSettings.AllowModeVoting = allowModeVoting; + ServerSettings.VoiceChatEnabled = voiceChatEnabled; + ServerSettings.AllowSubVoting = allowSubVoting; + ServerSettings.AllowModeVoting = allowModeVoting; - if (clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2POwnerPeer) { - Steam.SteamManager.UpdateLobby(serverSettings); + Steam.SteamManager.UpdateLobby(ServerSettings); } GUI.KeyboardDispatcher.Subscriber = prevDispatcher; @@ -2252,7 +2105,7 @@ namespace Barotrauma.Networking break; case ServerNetObject.ENTITY_EVENT: case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) + if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) { return; } @@ -2385,20 +2238,20 @@ namespace Barotrauma.Networking DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } - clientPeer.Send(outmsg, DeliveryMethod.Unreliable); + ClientPeer.Send(outmsg, DeliveryMethod.Unreliable); } private void SendIngameUpdate() { IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_INGAME); - outmsg.WriteBoolean(entityEventManager.MidRoundSyncingDone); + outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone); outmsg.WritePadBits(); outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(entityEventManager.LastReceivedID); + outmsg.WriteUInt16(EntityEventManager.LastReceivedID); outmsg.WriteUInt16(LastClientListUpdateID); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) @@ -2419,7 +2272,7 @@ namespace Barotrauma.Networking Character.Controlled?.ClientWriteInput(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); - entityEventManager.Write(outmsg, clientPeer?.ServerConnection); + EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection); chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2439,12 +2292,12 @@ namespace Barotrauma.Networking DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } - clientPeer.Send(outmsg, DeliveryMethod.Unreliable); + ClientPeer.Send(outmsg, DeliveryMethod.Unreliable); } public void SendChatMessage(ChatMessage msg) { - if (clientPeer?.ServerConnection == null) { return; } + if (ClientPeer?.ServerConnection == null) { return; } lastQueueChatMsgID++; msg.NetStateID = lastQueueChatMsgID; chatMsgQueue.Add(msg); @@ -2452,13 +2305,13 @@ namespace Barotrauma.Networking public void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Default) { - if (clientPeer?.ServerConnection == null) { return; } + if (ClientPeer?.ServerConnection == null) { return; } ChatMessage chatMessage = ChatMessage.Create( - gameStarted && myCharacter != null ? myCharacter.Name : Name, + GameStarted && myCharacter != null ? myCharacter.Name : Name, message, type, - gameStarted && myCharacter != null ? myCharacter : null); + GameStarted && myCharacter != null ? myCharacter : null); chatMessage.ChatMode = GameMain.ActiveChatMode; lastQueueChatMsgID++; @@ -2473,7 +2326,7 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN); msg.WriteBoolean((bool)waitForNextRoundRespawn); - clientPeer?.Send(msg, DeliveryMethod.Reliable); + ClientPeer?.Send(msg, DeliveryMethod.Reliable); } public void RequestFile(FileTransferType fileType, string file, string fileHash) @@ -2492,7 +2345,7 @@ namespace Barotrauma.Networking msg.WriteString(file ?? throw new ArgumentNullException(nameof(file))); msg.WriteString(fileHash ?? throw new ArgumentNullException(nameof(fileHash))); } - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void CancelFileTransfer(FileReceiver.FileTransferIn transfer) @@ -2514,7 +2367,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)transfer.ID); msg.WriteInt32(expecting); msg.WriteInt32(lastSeen); - clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); + ClientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); } public void CancelFileTransfer(int id) @@ -2523,7 +2376,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST); msg.WriteByte((byte)FileTransferMessageType.Cancel); msg.WriteByte((byte)id); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } private void OnFileReceived(FileReceiver.FileTransferIn transfer) @@ -2669,7 +2522,7 @@ namespace Barotrauma.Networking { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } - entityEventManager.CreateEvent(clientSerializable, extraData); + EntityEventManager.CreateEvent(clientSerializable, extraData); } public bool HasPermission(ClientPermissions permission) @@ -2696,11 +2549,9 @@ namespace Barotrauma.Networking return false; } - public override void Quit() + public void Quit() { - allowReconnect = false; - - if (clientPeer is SteamP2PClientPeer || clientPeer is SteamP2POwnerPeer) + if (ClientPeer is SteamP2PClientPeer || ClientPeer is SteamP2POwnerPeer) { SteamManager.LeaveLobby(); } @@ -2710,20 +2561,14 @@ namespace Barotrauma.Networking CampaignMode.StartRoundCancellationToken?.Cancel(); - clientPeer?.Close(); - clientPeer = null; + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + ClientPeer = null; - List activeTransfers = new List(FileReceiver.ActiveTransfers); - foreach (var fileTransfer in activeTransfers) + foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) { FileReceiver.StopTransfer(fileTransfer, deleteFile: true); } - if (HasPermission(ClientPermissions.ServerLog)) - { - serverSettings.ServerLog?.Save(); - } - if (ChildServerRelay.Process != null) { int checks = 0; @@ -2753,7 +2598,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); WriteCharacterInfo(msg, newName); msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer?.Send(msg, DeliveryMethod.Reliable); + ClientPeer?.Send(msg, DeliveryMethod.Reliable); } public void WriteCharacterInfo(IWriteMessage msg, string newName = null) @@ -2788,7 +2633,7 @@ namespace Barotrauma.Networking public void Vote(VoteType voteType, object data) { - if (clientPeer == null) { return; } + if (ClientPeer == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); @@ -2796,7 +2641,7 @@ namespace Barotrauma.Networking Voting.ClientWrite(msg, voteType, data); msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void VoteForKick(Client votedClient) @@ -2852,7 +2697,7 @@ namespace Barotrauma.Networking msg.WriteString(kickedName); msg.WriteString(reason); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public override void BanPlayer(string kickedName, string reason, TimeSpan? duration = null) @@ -2864,7 +2709,7 @@ namespace Barotrauma.Networking msg.WriteString(reason); msg.WriteDouble(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public override void UnbanPlayer(string playerName) @@ -2874,7 +2719,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)ClientPermissions.Unban); msg.WriteBoolean(true); msg.WritePadBits(); msg.WriteString(playerName); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public override void UnbanPlayer(Endpoint endpoint) @@ -2884,7 +2729,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)ClientPermissions.Unban); msg.WriteBoolean(false); msg.WritePadBits(); msg.WriteString(endpoint.StringRepresentation); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void UpdateClientPermissions(Client targetClient) @@ -2893,7 +2738,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManagePermissions); targetClient.WritePermissions(msg); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendCampaignState() @@ -2908,7 +2753,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendConsoleCommand(string command) @@ -2927,7 +2772,7 @@ namespace Barotrauma.Networking msg.WriteSingle(cursorWorldPos.X); msg.WriteSingle(cursorWorldPos.Y); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2941,7 +2786,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(false); //indicates round start msg.WriteBoolean(continueCampaign); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2957,7 +2802,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(isShuttle); msg.WritePadBits(); msg.WriteString(sub.MD5Hash.StringRepresentation); msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2978,7 +2823,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)modeIndex); msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings) @@ -2998,12 +2843,12 @@ namespace Barotrauma.Networking msg.WriteString(sub.MD5Hash.StringRepresentation); msg.WriteNetSerializableStruct(settings); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public void SetupLoadCampaign(string saveName) { - if (clientPeer == null) { return; } + if (ClientPeer == null) { return; } GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; GameMain.NetLobbyScreen.CampaignFrame.Visible = false; @@ -3014,7 +2859,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(false); msg.WritePadBits(); msg.WriteString(saveName); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -3028,7 +2873,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(true); //indicates round end msg.WriteBoolean(save); - clientPeer.Send(msg, DeliveryMethod.Reliable); + ClientPeer.Send(msg, DeliveryMethod.Reliable); } public bool SpectateClicked(GUIButton button, object userData) @@ -3044,7 +2889,7 @@ namespace Barotrauma.Networking if (button != null) { button.Enabled = false; } if (campaign != null) { LateCampaignJoin = true; } - if (clientPeer == null) { return false; } + if (ClientPeer == null) { return false; } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.WriteByte((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -3055,14 +2900,14 @@ namespace Barotrauma.Networking WriteCharacterInfo(readyToStartMsg); - clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); + ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); return false; } public bool SetReadyToStart(GUITickBox tickBox) { - if (gameStarted) + if (GameStarted) { tickBox.Parent.Visible = false; return false; @@ -3073,9 +2918,9 @@ namespace Barotrauma.Networking public bool ToggleEndRoundVote(GUITickBox tickBox) { - if (!gameStarted) return false; + if (!GameStarted) return false; - if (!serverSettings.AllowEndVoting || !HasSpawned) + if (!ServerSettings.AllowEndVoting || !HasSpawned) { tickBox.Visible = false; return false; @@ -3176,19 +3021,19 @@ namespace Barotrauma.Networking return true; } - public virtual void AddToGUIUpdateList() + public void AddToGUIUpdateList() { if (GUI.DisableHUD || GUI.DisableUpperHUD) return; - if (gameStarted && + if (GameStarted && Screen.Selected == GameMain.GameScreen) { inGameHUD.AddToGUIUpdateList(); GameMain.NetLobbyScreen.FileTransferFrame?.AddToGUIUpdateList(); } - serverSettings.AddToGUIUpdateList(); - if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); + ServerSettings.AddToGUIUpdateList(); + if (ServerSettings.ServerLog.LogFrame != null) ServerSettings.ServerLog.LogFrame.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.PlayerFrame?.AddToGUIUpdateList(); } @@ -3208,7 +3053,7 @@ namespace Barotrauma.Networking UpdateLogButtonVisibility(); - if (gameStarted && Screen.Selected == GameMain.GameScreen) + if (GameStarted && Screen.Selected == GameMain.GameScreen) { bool disableButtons = Character.Controlled?.SelectedItem?.GetComponent() is Controller c1 && c1.HideHUD || Character.Controlled?.SelectedSecondaryItem?.GetComponent() is Controller c2 && c2.HideHUD; @@ -3265,13 +3110,13 @@ namespace Barotrauma.Networking } } - public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) + public void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (GUI.DisableHUD || GUI.DisableUpperHUD) return; - if (fileReceiver != null && fileReceiver.ActiveTransfers.Count > 0) + if (FileReceiver != null && FileReceiver.ActiveTransfers.Count > 0) { - var transfer = fileReceiver.ActiveTransfers.First(); + var transfer = FileReceiver.ActiveTransfers.First(); GameMain.NetLobbyScreen.FileTransferFrame.Visible = true; GameMain.NetLobbyScreen.FileTransferFrame.UserData = transfer; GameMain.NetLobbyScreen.FileTransferTitle.Text = @@ -3288,7 +3133,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.FileTransferFrame.Visible = false; } - if (!gameStarted || Screen.Selected != GameMain.GameScreen) { return; } + if (!GameStarted || Screen.Selected != GameMain.GameScreen) { return; } inGameHUD.DrawManually(spriteBatch); @@ -3314,7 +3159,7 @@ namespace Barotrauma.Networking EndVoteTickBox.Text = endRoundVoteText; } - if (respawnManager != null) + if (RespawnManager != null) { LocalizedString respawnText = string.Empty; Color textColor = Color.White; @@ -3323,26 +3168,26 @@ namespace Barotrauma.Networking Character.Controlled == null && Level.Loaded?.Type != LevelData.LevelType.Outpost && (characterInfo == null || HasSpawned); - if (respawnManager.CurrentState == RespawnManager.State.Waiting) + if (RespawnManager.CurrentState == RespawnManager.State.Waiting) { - if (respawnManager.RespawnCountdownStarted) + if (RespawnManager.RespawnCountdownStarted) { - float timeLeft = (float)(respawnManager.RespawnTime - DateTime.Now).TotalSeconds; + float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds; respawnText = TextManager.GetWithVariable( - respawnManager.UsingShuttle && !respawnManager.ForceSpawnInMainSub ? + RespawnManager.UsingShuttle && !RespawnManager.ForceSpawnInMainSub ? "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); } - else if (respawnManager.PendingRespawnCount > 0) + else if (RespawnManager.PendingRespawnCount > 0) { respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", - ("[deadplayers]", respawnManager.PendingRespawnCount.ToString()), - ("[requireddeadplayers]", respawnManager.RequiredRespawnCount.ToString())); + ("[deadplayers]", RespawnManager.PendingRespawnCount.ToString()), + ("[requireddeadplayers]", RespawnManager.RequiredRespawnCount.ToString())); } } - else if (respawnManager.CurrentState == RespawnManager.State.Transporting && - respawnManager.ReturnCountdownStarted) + else if (RespawnManager.CurrentState == RespawnManager.State.Transporting && + RespawnManager.ReturnCountdownStarted) { - float timeLeft = (float)(respawnManager.ReturnTime - DateTime.Now).TotalSeconds; + float timeLeft = (float)(RespawnManager.ReturnTime - DateTime.Now).TotalSeconds; respawnText = timeLeft <= 0.0f ? "" : TextManager.GetWithVariable("RespawnShuttleLeavingIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); @@ -3390,7 +3235,7 @@ namespace Barotrauma.Networking }*/ } - public virtual bool SelectCrewCharacter(Character character, GUIComponent frame) + public bool SelectCrewCharacter(Character character, GUIComponent frame) { if (character == null) { return false; } @@ -3405,7 +3250,7 @@ namespace Barotrauma.Networking return true; } - public virtual bool SelectCrewClient(Client client, GUIComponent frame) + public bool SelectCrewClient(Client client, GUIComponent frame) { if (client == null || client.SessionId == SessionId) { return false; } CreateSelectionRelatedButtons(client, frame); @@ -3470,7 +3315,7 @@ namespace Barotrauma.Networking OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } }; } - else if (serverSettings.AllowVoteKick && client.AllowKicking) + else if (ServerSettings.AllowVoteKick && client.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), TextManager.Get("VoteToKick"), style: "GUIButtonSmall") @@ -3580,18 +3425,15 @@ namespace Barotrauma.Networking } break; } - clientPeer.Send(outMsg, DeliveryMethod.Reliable); + ClientPeer.Send(outMsg, DeliveryMethod.Reliable); - if (!eventErrorWritten) - { - WriteEventErrorData(error, expectedId, eventId, entityId); - eventErrorWritten = true; - } + WriteEventErrorData(error, expectedId, eventId, entityId); } private bool eventErrorWritten; private void WriteEventErrorData(ClientNetError error, UInt16 expectedID, UInt16 eventID, UInt16 entityID) { + if (eventErrorWritten) { return; } List errorLines = new List { error.ToString(), "" @@ -3681,12 +3523,14 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } File.WriteAllLines(filePath, errorLines); + + eventErrorWritten = true; } #if DEBUG public void ForceTimeOut() { - clientPeer?.ForceTimeOut(); + ClientPeer?.ForceTimeOut(); } #endif } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index e2ce04903..e6b45ac85 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -2,6 +2,8 @@ using Barotrauma.Steam; using System; using System.Collections.Immutable; +using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -10,37 +12,14 @@ namespace Barotrauma.Networking public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; - public delegate void MessageCallback(IReadMessage message); - - public delegate void DisconnectCallback(bool disableReconnect); - - public delegate void DisconnectMessageCallback(string message); - - public delegate void PasswordCallback(int salt, int retries); - - public delegate void InitializationCompleteCallback(); - - [Obsolete("TODO: delete in nr3-layer-1-2-cleanup")] - public readonly struct Callbacks + public readonly record struct Callbacks( + Callbacks.MessageCallback OnMessageReceived, + Callbacks.DisconnectCallback OnDisconnect, + Callbacks.InitializationCompleteCallback OnInitializationComplete) { - 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; - } + public delegate void MessageCallback(IReadMessage message); + public delegate void DisconnectCallback(PeerDisconnectPacket disconnectPacket); + public delegate void InitializationCompleteCallback(); } protected readonly Callbacks callbacks; @@ -51,6 +30,8 @@ namespace Barotrauma.Networking protected readonly bool isOwner; protected readonly Option ownerKey; + protected bool isActive; + public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) { ServerEndpoint = serverEndpoint; @@ -60,7 +41,7 @@ namespace Barotrauma.Networking } public abstract void Start(); - public abstract void Close(string? msg = null, bool disableReconnect = false); + public abstract void Close(PeerDisconnectPacket peerDisconnectPacket); public abstract void Update(float deltaTime); public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void SendPassword(string password); @@ -71,6 +52,12 @@ namespace Barotrauma.Networking public bool ContentPackageOrderReceived { get; protected set; } protected int passwordSalt; protected Steamworks.AuthTicket? steamAuthTicket; + private GUIMessageBox? passwordMsgBox; + + public bool WaitingForPassword + => isActive && initializationStep == ConnectionInitialization.Password + && passwordMsgBox != null + && GUIMessageBox.MessageBoxes.Contains(passwordMsgBox); public struct IncomingInitializationMessage { @@ -112,8 +99,9 @@ namespace Barotrauma.Networking } case ConnectionInitialization.ContentPackageOrder: { - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || - initializationStep == ConnectionInitialization.Password) + if (initializationStep + is ConnectionInitialization.SteamTicketAndVersion + or ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; } @@ -155,10 +143,57 @@ namespace Barotrauma.Networking var passwordPacket = INetSerializableStruct.Read(inc.Message); + if (WaitingForPassword) { return; } + passwordPacket.Salt.TryUnwrap(out passwordSalt); passwordPacket.RetriesLeft.TryUnwrap(out var retries); - callbacks.OnRequestPassword.Invoke(passwordSalt, retries); + LocalizedString pwMsg = TextManager.Get("PasswordRequired"); + + passwordMsgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) + { + Censor = true + }; + + if (retries > 0) + { + var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); + incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); + passwordHolder.Recalculate(); + } + + passwordMsgBox.Content.Recalculate(); + passwordMsgBox.Content.RectTransform.MinSize = new Point(0, passwordMsgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); + passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y)); + + var okButton = passwordMsgBox.Buttons[0]; + okButton.OnClicked += (_, __) => + { + SendPassword(passwordBox.Text); + return true; + }; + okButton.OnClicked += passwordMsgBox.Close; + + var cancelButton = passwordMsgBox.Buttons[1]; + cancelButton.OnClicked = (_, __) => + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + passwordMsgBox?.Close(); passwordMsgBox = null; + + return true; + }; + + passwordBox.OnEnterPressed += (_, __) => + { + okButton.OnClicked.Invoke(okButton, okButton.UserData); + return true; + }; + + passwordBox.Select(); + break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 5cb7795d0..a95afd553 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -9,7 +9,6 @@ namespace Barotrauma.Networking { internal sealed class LidgrenClientPeer : ClientPeer { - private bool isActive; private NetClient? netClient; private readonly NetPeerConfiguration netPeerConfiguration; @@ -94,7 +93,7 @@ namespace Barotrauma.Networking if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) { - Close(); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { @@ -140,8 +139,10 @@ namespace Barotrauma.Networking var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); - if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) + if (packetHeader.IsConnectionInitializationStep()) { + if (initializationStep == ConnectionInitialization.Success) { return; } + ReadConnectionInitializationStep(new IncomingInitializationMessage { InitializationStep = initialization ?? throw new Exception("Initialization step missing"), @@ -170,8 +171,9 @@ namespace Barotrauma.Networking { case NetConnectionStatus.Disconnected: string disconnectMsg = inc.ReadString(); - Close(disconnectMsg); - callbacks.OnDisconnectMessageReceived.Invoke(disconnectMsg); + var peerDisconnectPacket = + PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg); + Close(peerDisconnectPacket.Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown))); break; } } @@ -198,7 +200,7 @@ namespace Barotrauma.Networking SendMsgInternal(headers, body); } - public override void Close(string? msg = null, bool disableReconnect = false) + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } @@ -206,13 +208,13 @@ namespace Barotrauma.Networking isActive = false; - netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); + netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation()); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; - callbacks.OnDisconnect.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } 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 64c8b29b5..e817616b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -10,7 +10,6 @@ namespace Barotrauma.Networking { internal sealed class SteamP2PClientPeer : ClientPeer { - private bool isActive; private readonly SteamId hostSteamId; private double timeout; private double heartbeatTimer; @@ -97,8 +96,7 @@ namespace Barotrauma.Networking if (steamId != hostSteamId.Value) { return; } - Close($"SteamP2P connection failed: {error}"); - callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); + Close(PeerDisconnectPacket.SteamP2PError(error)); } private void OnP2PData(ulong steamId, byte[] data, int dataLength) @@ -117,8 +115,10 @@ namespace Barotrauma.Networking if (!packetHeader.IsServerMessage()) { return; } - if (packetHeader.IsConnectionInitializationStep() && initialization.HasValue) + if (packetHeader.IsConnectionInitializationStep()) { + if (!initialization.HasValue) { return; } + var relayPacket = INetSerializableStruct.Read(inc); SteamManager.JoinLobby(relayPacket.LobbyID, false); @@ -127,7 +127,7 @@ namespace Barotrauma.Networking incomingInitializationMessages.Add(new IncomingInitializationMessage { InitializationStep = initialization.Value, - Message = relayPacket.Message.GetReadMessage() + Message = relayPacket.Message.GetReadMessageUncompressed() }); } } @@ -138,13 +138,12 @@ namespace Barotrauma.Networking else if (packetHeader.IsDisconnectMessage()) { PeerDisconnectPacket packet = INetSerializableStruct.Read(inc); - Close(packet.Message); - callbacks.OnDisconnectMessageReceived.Invoke(packet.Message); + Close(packet); } else { var packet = INetSerializableStruct.Read(inc); - incomingDataMessages.Add(packet.GetReadMessage()); + incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!)); } } @@ -170,14 +169,12 @@ namespace Barotrauma.Networking { if (state.P2PSessionError != Steamworks.P2PSessionError.None) { - Close($"SteamP2P error code: {state.P2PSessionError}"); - callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state.P2PSessionError}"); + Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError)); } } else { - Close("SteamP2P connection could not be established"); - callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PError.ToString()); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } connectionStatusTimer = 1.0f; @@ -212,8 +209,7 @@ namespace Barotrauma.Networking if (timeout < 0.0) { - Close("Timed out"); - callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut)); return; } @@ -221,20 +217,26 @@ namespace Barotrauma.Networking { if (incomingDataMessages.Count > 0) { - var incomingMessage = incomingDataMessages.First(); - byte incomingHeader = incomingMessage.LengthBytes > 0 ? incomingMessage.PeekByte() : (byte)0; - if (ContentPackageOrderReceived) + void initializationError(string errorMsg, string analyticsTag) { -#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); + GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError(errorMsg); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + } + + if (!ContentPackageOrderReceived) + { + initializationError( + errorMsg: "Error during connection initialization: completed initialization before receiving content package order.", + analyticsTag: "ContentPackageOrderNotReceived"); + return; } if (ServerContentPackages.Length == 0) { - 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); + initializationError( + errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.", + analyticsTag: "NoContentPackages"); + return; } callbacks.OnInitializationComplete.Invoke(); initializationStep = ConnectionInitialization.Success; @@ -320,7 +322,7 @@ namespace Barotrauma.Networking SendMsgInternal(headers, body); } - public override void Close(string? msg = null, bool disableReconnect = false) + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } @@ -334,12 +336,7 @@ namespace Barotrauma.Networking PacketHeader = PacketHeader.IsDisconnectMessage, Initialization = null }; - var body = new PeerDisconnectPacket - { - Message = msg ?? "Disconnected" - }; - - SendMsgInternal(headers, body); + SendMsgInternal(headers, peerDisconnectPacket); Thread.Sleep(100); @@ -349,7 +346,7 @@ namespace Barotrauma.Networking steamAuthTicket?.Cancel(); steamAuthTicket = null; - callbacks.OnDisconnect.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 23deeacce..440e3126e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -9,8 +9,6 @@ namespace Barotrauma.Networking { sealed class SteamP2POwnerPeer : ClientPeer { - private bool isActive; - private readonly SteamId selfSteamID; private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); @@ -68,6 +66,7 @@ namespace Barotrauma.Networking : throw new InvalidOperationException("Steamworks not initialized"); } + public override void Start() { if (isActive) { return; } @@ -93,22 +92,13 @@ namespace Barotrauma.Networking private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) { RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); - DebugConsole.Log($"{steamId} validation: {status}, {remotePeer != null}"); if (remotePeer == null) { return; } - if (remotePeer.Authenticated) - { - if (status != Steamworks.AuthResponse.OK) - { - DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication status changed: {status}"); - } - - return; - } - if (status == Steamworks.AuthResponse.OK) { + if (remotePeer.Authenticated) { return; } + SteamId ownerSteamId = new SteamId(ownerId); remotePeer.OwnerSteamId = Option.Some(ownerSteamId); remotePeer.Authenticated = true; @@ -126,7 +116,7 @@ namespace Barotrauma.Networking } else { - DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication failed: {status}"); + DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status)); } } @@ -171,7 +161,7 @@ namespace Barotrauma.Networking Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam auth session failed to start: {authSessionStartState}"); + DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); return; } } @@ -197,9 +187,9 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true)) + if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false })) { - Close(); + Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { @@ -279,7 +269,7 @@ namespace Barotrauma.Networking if (packetHeader.IsDisconnectMessage()) { var packet = INetSerializableStruct.Read(inc); - DisconnectPeer(peer, packet.Message); + DisconnectPeer(peer, packet); return; } @@ -366,30 +356,20 @@ namespace Barotrauma.Networking } } - private void DisconnectPeer(RemotePeer peer, string msg) + private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket) { - if (!string.IsNullOrWhiteSpace(msg)) - { - peer.DisconnectTime ??= Timing.TotalTime + 1.0; + peer.DisconnectTime ??= Timing.TotalTime + 1.0; - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.WriteNetSerializableStruct(new PeerPacketHeaders - { - DeliveryMethod = DeliveryMethod.Reliable, - PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage - }); - outMsg.WriteNetSerializableStruct(new PeerDisconnectPacket - { - Message = msg - }); - - Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; - } - else + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders { - ClosePeerSession(peer); - } + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage + }); + outMsg.WriteNetSerializableStruct(peerDisconnectPacket); + + Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } private void ClosePeerSession(RemotePeer peer) @@ -403,7 +383,7 @@ namespace Barotrauma.Networking //owner doesn't send passwords } - public override void Close(string? msg = null, bool disableReconnect = false) + public override void Close(PeerDisconnectPacket peerDisconnectPacket) { if (!isActive) { return; } @@ -411,7 +391,7 @@ namespace Barotrauma.Networking for (int i = remotePeers.Count - 1; i >= 0; i--) { - DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + DisconnectPeer(remotePeers[i], peerDisconnectPacket); } Thread.Sleep(100); @@ -423,7 +403,7 @@ namespace Barotrauma.Networking ChildServerRelay.ClosePipes(); - callbacks.OnDisconnect.Invoke(disableReconnect); + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); SteamManager.LeaveLobby(); Steamworks.SteamNetworking.ResetActions(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs deleted file mode 100644 index 93e854e66..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ /dev/null @@ -1,517 +0,0 @@ -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Barotrauma.Networking -{ - class ServerInfo - { - public Endpoint Endpoint; - - #region TODO: genericize - public int QueryPort; - public UInt64 LobbyID; - public Steamworks.Data.NetPingLocation? PingLocation; - #endregion - - public bool OwnerVerified; - - private string serverName; - public string ServerName - { - get { return serverName; } - set - { - serverName = value; - if (serverName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } - } - } - - public string ServerMessage; - public bool GameStarted; - public int PlayerCount; - public int MaxPlayers; - public bool HasPassword; - - public bool PingChecked; - public int Ping = -1; - - //null value means that the value isn't known (the server may be using - //an old version of the game that didn't report these values or the FetchRules query to Steam may not have finished yet) - // TODO: death to Nullable!!!! - public SelectionMode? ModeSelectionMode; - public SelectionMode? SubSelectionMode; - public bool? AllowSpectating; - public bool? VoipEnabled; - public bool? KarmaEnabled; - public bool? FriendlyFireEnabled; - public bool? AllowRespawn; - public YesNoMaybe? TraitorsEnabled; - public Identifier GameMode; - public PlayStyle? PlayStyle; - - public bool Recent; - public bool Favorite; - - public bool? RespondedToSteamQuery = null; - - public Steamworks.Friend? SteamFriend; - public Steamworks.SteamMatchmakingPingResponse MatchmakingPingResponse; - - public string GameVersion; - public List ContentPackageNames - { - get; - private set; - } = new List(); - public List ContentPackageHashes - { - get; - private set; - } = new List(); - public List ContentPackageWorkshopIds - { - get; - private set; - } = new List(); - - public void CreatePreviewWindow(GUIFrame frame) - { - if (frame == null) { return; } - - frame.ClearChildren(); - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) - { - ToolTip = ServerName, - CanBeFocused = false - }; - title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); - - GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), - "", null, "GUIServerListFavoriteTickBox") - { - Selected = Favorite, - ToolTip = TextManager.Get(Favorite ? "removefromfavorites" : "addtofavorites"), - OnSelected = (tickbox) => - { - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(this); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(this); - } - tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); - return true; - } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), - string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)) - { - CanBeFocused = false - }; - - bool hidePlaystyleBanner = !PlayStyle.HasValue; - if (!hidePlaystyleBanner) - { - PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; - Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; - float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; - var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform), - playStyleBannerSprite, null, true); - - var playStyleName = new GUITextBlock( - new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) - { RelativeOffset = new Vector2(0.0f, 0.06f) }, - TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), - TextManager.Get("servertag." + playStyle)), textColor: Color.White, - font: GUIStyle.SmallFont, textAlignment: Alignment.Center, - color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); - playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); - playStyleName.RectTransform.IsFixedSize = true; - } - - var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - Endpoint.ServerTypeString, - textAlignment: Alignment.TopLeft) - { - CanBeFocused = false - }; - serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); - - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) - { - Stretch = true - }; - // playstyle tags ----------------------------------------------------------------------------- - - var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f, - CanBeFocused = true - }; - - var playStyleTags = GetPlayStyleTags(); - foreach (string tag in playStyleTags) - { - if (!ServerListScreen.PlayStyleIcons.ContainsKey(tag)) { continue; } - - new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform), - ServerListScreen.PlayStyleIcons[tag], scaleToFit: true) - { - ToolTip = TextManager.Get("servertagdescription." + tag), - Color = ServerListScreen.PlayStyleIconColors[tag] - }; - } - - playStyleContainer.Recalculate(); - - // ----------------------------------------------------------------------------- - - float elementHeight = 0.075f; - - // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); - - var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; - var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true) - { - CanBeFocused = false - }; - serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; - msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; - - var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); - new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), - TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), - textAlignment: Alignment.Right); - - GUITextBlock playStyleText = null; - if (hidePlaystyleBanner && PlayStyle.HasValue) - { - PlayStyle playStyle = PlayStyle.Value; - playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); - new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); - } - - var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection")); - new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - - var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection")); - new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(!ModeSelectionMode.HasValue ? "Unknown" : ModeSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - - if (gameMode.TextSize.X + gameMode.GetChild().TextSize.X > gameMode.Rect.Width || - subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || - modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) - { - gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; - gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; - if (playStyleText != null) - { - playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; - } - } - - var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating")) - { - CanBeFocused = false - }; - if (!AllowSpectating.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowSpectating.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - allowSpectating.Selected = AllowSpectating.Value; - - var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) - { - CanBeFocused = false - }; - if (!AllowRespawn.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowRespawn.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - allowRespawn.Selected = AllowRespawn.Value; - - /*var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled")) - { - CanBeFocused = false - }; - if (!VoipEnabled.HasValue) - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), voipEnabledTickBox.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); - else - voipEnabledTickBox.Selected = VoipEnabled.Value;*/ - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); - - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (component, o) => false - }; - if (ContentPackageNames.Count == 0) - { - new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else - { - for (int i = 0; i < ContentPackageNames.Count; i++) - { - var packageText = new GUITickBox( - new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) - { MinSize = new Point(0, 15) }, - ContentPackageNames[i]) - { - Enabled = false - }; - packageText.Box.Enabled = true; - packageText.TextBlock.Enabled = true; - if (i < ContentPackageHashes.Count) - { - if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i])) - { - packageText.TextColor = GUIStyle.Green; - packageText.Selected = true; - } - //workshop download link found - else if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) - { - packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); - } - else //no package or workshop download link found (TODO: update text to say that they could be downloaded through the server) - { - packageText.TextColor = GameMain.VanillaContent.NameMatches(ContentPackageNames[i]) ? GUIStyle.Red : GUIStyle.Yellow; - packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", - ("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i])); - } - } - } - } - - // ----------------------------------------------------------------------------- - - foreach (GUIComponent c in content.Children) - { - if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; } - } - } - - public IEnumerable GetPlayStyleTags() - { - List tags = new List(); - if (KarmaEnabled.HasValue) - { - tags.Add(KarmaEnabled.Value ? "karma.true" : "karma.false"); - } - if (TraitorsEnabled.HasValue) - { - tags.Add(TraitorsEnabled.Value == YesNoMaybe.Maybe ? - "traitors.maybe" : - (TraitorsEnabled.Value == YesNoMaybe.Yes ? "traitors.true" : "traitors.false")); - } - if (VoipEnabled.HasValue) - { - tags.Add(VoipEnabled.Value ? "voip.true" : "voip.false"); - } - if (FriendlyFireEnabled.HasValue) - { - tags.Add(FriendlyFireEnabled.Value ? "friendlyfire.true" : "friendlyfire.false"); - } - if (ContentPackageNames.Count > 0) - { - tags.Add(ContentPackageNames.Count > 1 || !GameMain.VanillaContent.NameMatches(ContentPackageNames[0]) ? "modded.true" : "modded.false"); - } - return tags; - } - - public static ServerInfo FromXElement(XElement element) - { - 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", ""), - Endpoint = endpoint, - QueryPort = !string.IsNullOrEmpty(element.GetAttributeString("QueryPort", string.Empty)) ? element.GetAttributeInt("QueryPort", 0) : 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 - }; - - if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; } - if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; } - if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; } - if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = modeSelectionTemp; } - if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; } - if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; } - if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; } - - return info; - } - - public void QueryLiveInfo(Action onServerRulesReceived, Action onQueryDone) - { - if (!SteamManager.IsInitialized) { return; } - - if (QueryPort != 0 && Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ipAddress } }) - { - if (MatchmakingPingResponse is { QueryActive: true }) - { - MatchmakingPingResponse.Cancel(); - } - - MatchmakingPingResponse = new Steamworks.SteamMatchmakingPingResponse( - (server) => - { - ServerName = server.Name; - RespondedToSteamQuery = true; - PlayerCount = server.Players; - MaxPlayers = server.MaxPlayers; - HasPassword = server.Passworded; - PingChecked = true; - Ping = server.Ping; - LobbyID = 0; - TaskPool.Add("QueryServerRules (QueryLiveInfo)", server.QueryRulesAsync(), - (t) => - { - onQueryDone(this); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + ServerName); - return; - } - - t.TryGetResult(out Dictionary rules); - SteamManager.AssignServerRulesToServerInfo(rules, this); - - onServerRulesReceived(this); - }); - }, - () => - { - RespondedToSteamQuery = false; - }); - - MatchmakingPingResponse.HQueryPing(ipAddress, QueryPort); - } - else if (Endpoint is SteamP2PEndpoint { SteamId: var ownerId }) - { - SteamFriend ??= new Steamworks.Friend(ownerId.Value); - if (LobbyID == 0) - { - TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(), - (t) => - { - onQueryDone(this); - if ((SteamFriend?.IsPlayingThisGame ?? false) && ((SteamFriend?.GameInfo?.Lobby?.Id ?? 0) != 0)) - { - LobbyID = SteamFriend?.GameInfo?.Lobby?.Id.Value ?? 0; - Steamworks.SteamMatchmaking.OnLobbyDataChanged += UpdateInfoFromSteamworksLobby; - SteamFriend?.GameInfo?.Lobby?.Refresh(); - } - else - { - RespondedToSteamQuery = false; - } - }); - } - else - { - onQueryDone(this); - } - } - } - - private void UpdateInfoFromSteamworksLobby(Steamworks.Data.Lobby lobby) - { - if (lobby.Id != LobbyID) { return; } - Steamworks.SteamMatchmaking.OnLobbyDataChanged -= UpdateInfoFromSteamworksLobby; - if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { return; } - bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword); - int.TryParse(lobby.GetData("playercount"), out int currPlayers); - int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers); - - 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"); - PlayerCount = currPlayers; - MaxPlayers = maxPlayers; - HasPassword = hasPassword; - RespondedToSteamQuery = true; - PingChecked = false; - OwnerVerified = true; - - SteamManager.AssignLobbyDataToServerInfo(lobby, this); - } - - public XElement ToXElement() - { - if (Endpoint is null) - { - return null; //can't save this one since it's not set up correctly - } - - XElement element = new XElement("ServerInfo"); - - element.SetAttributeValue("ServerName", ServerName); - element.SetAttributeValue("ServerMessage", ServerMessage); - 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 (TraitorsEnabled.HasValue) { element.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.Value.ToString()); } - if (SubSelectionMode.HasValue) { element.SetAttributeValue("SubSelectionMode", SubSelectionMode.Value.ToString()); } - if (ModeSelectionMode.HasValue) { element.SetAttributeValue("ModeSelectionMode", ModeSelectionMode.Value.ToString()); } - if (VoipEnabled.HasValue) { element.SetAttributeValue("VoipEnabled", VoipEnabled.Value.ToString()); } - if (KarmaEnabled.HasValue) { element.SetAttributeValue("KarmaEnabled", KarmaEnabled.Value.ToString()); } - if (FriendlyFireEnabled.HasValue) { element.SetAttributeValue("FriendlyFireEnabled", FriendlyFireEnabled.Value.ToString()); } - element.SetAttributeValue("HasPassword", HasPassword.ToString()); - - return element; - } - - public override bool Equals(object obj) - { - return obj is ServerInfo other ? Equals(other) : base.Equals(obj); - } - - public bool Equals(ServerInfo other) - { - return - other.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 other.Endpoint == Endpoint; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs new file mode 100644 index 000000000..1a1753091 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma +{ + abstract class FriendProvider + { + public abstract ServerListScreen.FriendInfo[] RetrieveFriends(); + public abstract void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize); + public abstract string GetUserName(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs new file mode 100644 index 000000000..9026de250 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs @@ -0,0 +1,67 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + class SteamFriendProvider : FriendProvider + { + private static ServerListScreen.FriendInfo FromSteamFriend(Steamworks.Friend steamFriend) + => new ServerListScreen.FriendInfo( + steamFriend.Name, + new SteamId(steamFriend.Id), + steamFriend.State switch + { + Steamworks.FriendState.Offline => ServerListScreen.FriendInfo.Status.Offline, + Steamworks.FriendState.Invisible => ServerListScreen.FriendInfo.Status.Offline, + _ when steamFriend.IsPlayingThisGame => ServerListScreen.FriendInfo.Status.PlayingBarotrauma, + _ when steamFriend.GameInfo is { GameID: var gameId } && gameId > 0 => ServerListScreen.FriendInfo.Status.PlayingAnotherGame, + _ => ServerListScreen.FriendInfo.Status.NotPlaying + }) + { + ServerName = steamFriend.GetRichPresence("servername"), + ConnectCommand = steamFriend.GetRichPresence("connect") is { } connectCmd + ? ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCmd)) + : Option.None() + }; + + public override ServerListScreen.FriendInfo[] RetrieveFriends() + => SteamManager.IsInitialized + ? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToArray() + : Array.Empty(); + + public override void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize) + { + if (!(friend.Id is SteamId steamId)) { return; } + + Func> avatarFunc = avatarSize switch + { + ServerListScreen.AvatarSize.Small => Steamworks.SteamFriends.GetSmallAvatarAsync, + ServerListScreen.AvatarSize.Medium => Steamworks.SteamFriends.GetMediumAvatarAsync, + ServerListScreen.AvatarSize.Large => Steamworks.SteamFriends.GetLargeAvatarAsync, + }; + TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(steamId.Value), task => + { + if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } + if (!(img is { } avatarImage)) { return; } + + if (friend.Avatar.TryUnwrap(out var prevAvatar)) + { + prevAvatar.Remove(); + } + + #warning TODO: create an avatar atlas? + var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); + avatarTexture.SetData(avatarImage.Data); + friend.Avatar = Option.Some(new Sprite(avatarTexture, null, null)); + }); + } + + public override string GetUserName() + => SteamManager.GetUsername(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs new file mode 100644 index 000000000..4f099bf4e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -0,0 +1,196 @@ +using Barotrauma.Steam; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; +using Socket = System.Net.Sockets.Socket; + +namespace Barotrauma.Networking +{ + static class PingUtils + { + private static readonly Dictionary activePings = new Dictionary(); + + private static bool steamPingInfoReady; + + public static void QueryPingData() + { + steamPingInfoReady = false; + if (SteamManager.IsInitialized) + { + TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task => + { + steamPingInfoReady = true; + }); + } + } + + public static void GetServerPing(ServerInfo serverInfo, Action onPingDiscovered) + { + if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } + + switch (serverInfo.Endpoint) + { + case LidgrenEndpoint { NetEndpoint: { Address: var address } }: + GetIPAddressPing(serverInfo, address, onPingDiscovered); + break; + case SteamP2PEndpoint steamP2PEndpoint: + TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", + EstimateSteamLobbyPing(serverInfo), + t => + { + if (!t.TryGetResult(out Option ping)) { return; } + serverInfo.Ping = ping; + onPingDiscovered(serverInfo); + }); + break; + } + } + + private readonly ref struct LobbyDataChangedEventHandler + { + private readonly Action action; + + public LobbyDataChangedEventHandler(Action action) + { + this.action = action; + Steamworks.SteamMatchmaking.OnLobbyDataChanged += action; + } + + public void Dispose() + { + Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action; + } + } + + public static async Task GetSteamLobbyForUser(SteamId steamId) + { + var steamFriend = new Steamworks.Friend(steamId.Value); + await steamFriend.RequestInfoAsync(); + + var friendLobby = steamFriend.GameInfo?.Lobby; + if (!(friendLobby is { } lobby)) { return null; } + + bool waiting = true; + Lobby loadedLobby = default; + + void finishWaiting(Steamworks.Data.Lobby l) + { + loadedLobby = l; + waiting = false; + } + + using (new LobbyDataChangedEventHandler(finishWaiting)) + { + lobby.Refresh(); + + for (int i = 0;; i++) + { + if (!waiting) { break; } + if (i >= 100) { return null; } + } + } + + return loadedLobby; + } + + private static async Task> EstimateSteamLobbyPing(ServerInfo serverInfo) + { + if (!(serverInfo.Endpoint is SteamP2PEndpoint { SteamId: var ownerId })) { return Option.None(); } + while (!steamPingInfoReady) { await Task.Delay(50); } + + Lobby lobby; + + if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src)) + { + lobby = src.Lobby; + } + else + { + var friendLobby = await GetSteamLobbyForUser(ownerId); + if (friendLobby is null) { return Option.None(); } + lobby = friendLobby.Value; + } + + var pingLocation = NetPingLocation.TryParseFromString(lobby.GetData("pinglocation")); + + if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue) + { + int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value); + return ping >= 0 ? Option.Some(ping) : Option.None(); + } + else + { + return Option.None(); + } + } + + private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action onPingDiscovered) + { + lock (activePings) + { + if (activePings.ContainsKey(address)) { return; } + activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0); + } + + serverInfo.Ping = Option.None(); + + TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), + rtt => + { + if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option.None(); } + onPingDiscovered(serverInfo); + lock (activePings) + { + activePings.Remove(address); + } + }); + } + + private static async Task> PingServerAsync(IPAddress ipAddress, int timeOut) + { + await Task.Yield(); + bool shouldGo = false; + while (!shouldGo) + { + lock (activePings) + { + shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; + } + await Task.Delay(25); + } + + if (ipAddress == null) { return Option.None(); } + + //don't attempt to ping if the address is IPv6 and it's not supported + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } + + Ping ping = new Ping(); + byte[] buffer = new byte[32]; + try + { + PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true)); + + return pingReply.Status switch + { + IPStatus.Success => Option.Some((int)pingReply.RoundtripTime), + _ => Option.None(), + }; + } + catch (Exception ex) + { + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); +#if DEBUG + DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); +#endif + + return Option.None(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs new file mode 100644 index 000000000..8b1e9e2d3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -0,0 +1,509 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Steam; + +namespace Barotrauma.Networking +{ + sealed class ServerInfo : ISerializableEntity + { + public abstract class DataSource + { + public static Option Parse(XElement element) + => ReflectionUtils.ParseDerived(element); + public abstract void Write(XElement element); + } + + public Endpoint Endpoint { get; private set; } + + public Option MetadataSource = Option.None(); + + [Serialize("", IsPropertySaveable.Yes)] + public string ServerName { get; set; } = ""; + + [Serialize("", IsPropertySaveable.Yes)] + public string ServerMessage { get; set; } = ""; + + public int PlayerCount { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int MaxPlayers { get; set; } + + public bool GameStarted { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool HasPassword { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier GameMode { get; set; } + + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] + public SelectionMode ModeSelectionMode { get; set; } + + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] + public SelectionMode SubSelectionMode { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowSpectating { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool VoipEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool KarmaEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool FriendlyFireEnabled { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowRespawn { get; set; } + + [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] + public YesNoMaybe TraitorsEnabled { get; set; } + + [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] + public PlayStyle PlayStyle { get; set; } + + public Version GameVersion { get; set; } = new Version(0,0,0,0); + + public Option Ping = Option.None(); + + public bool Checked = false; + + public readonly struct ContentPackageInfo + { + public readonly string Name; + public readonly string Hash; + public readonly Option Id; + + public ContentPackageInfo(string name, string hash, Option id) + { + Name = name; + Hash = hash; + Id = id; + } + + public ContentPackageInfo(ContentPackage pkg) + { + Name = pkg.Name; + Hash = pkg.Hash.StringRepresentation; + Id = pkg.UgcId; + } + } + + public ImmutableArray ContentPackages; + + public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name)); + + public ServerInfo(Endpoint endpoint) + { + SerializableProperties = SerializableProperty.GetProperties(this); + Endpoint = endpoint; + ContentPackages = ImmutableArray.Empty; + } + + public static ServerInfo FromServerConnection(NetworkConnection connection, ServerSettings serverSettings) + { + var serverInfo = new ServerInfo(connection.Endpoint) + { + GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty, + GameStarted = Screen.Selected != GameMain.NetLobbyScreen, + GameVersion = GameMain.Version, + PlayerCount = GameMain.Client.ConnectedClients.Count, + ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ContentPackageInfo(p)).ToImmutableArray(), + Ping = GameMain.Client.Ping, + + // ------------------------------------- + // Settings that cannot be copied via + // SerializableProperty because they do + // not implement the attribute + ServerName = serverSettings.ServerName, + ServerMessage = serverSettings.ServerMessageText, + // ------------------------------------- + // Settings that cannot be copied via + // SerializableProperty due to name mismatch + HasPassword = serverSettings.HasPassword, + VoipEnabled = serverSettings.VoiceChatEnabled, + FriendlyFireEnabled = serverSettings.AllowFriendlyFire, + // ------------------------------------- + + Checked = true + }; + + var serverInfoSerializableProperties + = SerializableProperty.GetProperties(serverInfo); + var serverSettingsSerializableProperties + = SerializableProperty.GetProperties(serverSettings); + + var intersection = serverInfoSerializableProperties.Keys + .Where(serverSettingsSerializableProperties.ContainsKey); + + foreach (var key in intersection) + { + var propToGet = serverSettingsSerializableProperties[key]; + var propToSet = serverInfoSerializableProperties[key]; + if (!propToGet.PropertyInfo.CanRead) { continue; } + if (!propToSet.PropertyInfo.CanWrite) { continue; } + propToSet.SetValue( + serverInfo, + propToGet.GetValue(serverSettings)); + } + + return serverInfo; + } + + public void CreatePreviewWindow(GUIFrame frame) + { + frame.ClearChildren(); + + var serverListScreen = GameMain.ServerListScreen; + + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) + { + ToolTip = ServerName, + CanBeFocused = false + }; + title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); + + bool isFavorite = serverListScreen.IsFavorite(this); + + static LocalizedString favoriteTickBoxToolTip(bool isFavorite) + => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); + + GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), + "", null, "GUIServerListFavoriteTickBox") + { + UserData = this, + Selected = isFavorite, + ToolTip = favoriteTickBoxToolTip(isFavorite), + OnSelected = tickbox => + { + ServerInfo info = (ServerInfo)tickbox.UserData; + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); + return true; + } + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), + GameVersion.ToString())) + { + CanBeFocused = false + }; + + PlayStyle playStyle = PlayStyle; + Sprite? playStyleBannerSprite = GUIStyle.GetComponentStyle($"PlayStyleBanner.{playStyle}")?.GetSprite(GUIComponent.ComponentState.None); + + GUIComponent playStyleBanner; + Color playStyleBannerColor; + if (playStyleBannerSprite != null) + { + float playStyleBannerAspectRatio = (float)playStyleBannerSprite.SourceRect.Width / (float)playStyleBannerSprite.SourceRect.Height; + playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), frame.RectTransform, scaleBasis: ScaleBasis.BothWidth), + playStyleBannerSprite, null, true); + playStyleBannerColor = playStyleBannerSprite.SourceElement.GetAttributeColor("bannercolor", Color.Black); + } + else + { + playStyleBanner = new GUIFrame(new RectTransform((1.0f, 0.2f), frame.RectTransform), style: null) + { + Color = Color.Black, + DisabledColor = Color.Black, + OutlineColor = Color.Black, + PressedColor = Color.Black, + SelectedColor = Color.Black, + HoverColor = Color.Black + }; + playStyleBannerColor = Color.Black; + } + + var playStyleName = new GUITextBlock( + new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) + { RelativeOffset = new Vector2(0.0f, 0.06f) }, + TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), + TextManager.Get($"servertag.{playStyle}")), textColor: Color.White, + font: GUIStyle.SmallFont, textAlignment: Alignment.Center, + color: playStyleBannerColor, style: "GUISlopedHeader"); + playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); + playStyleName.RectTransform.IsFixedSize = true; + + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + Endpoint?.ServerTypeString ?? string.Empty, + textAlignment: Alignment.TopLeft) + { + CanBeFocused = false + }; + serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); + + var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) + { + Stretch = true + }; + // playstyle tags ----------------------------------------------------------------------------- + + var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f, + CanBeFocused = true + }; + + var playStyleTags = GetPlayStyleTags(); + foreach (var tag in playStyleTags) + { + var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}") + ?.GetSprite(GUIComponent.ComponentState.None); + if (playStyleIcon is null) { continue; } + + new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform), + playStyleIcon, scaleToFit: true) + { + ToolTip = TextManager.Get($"servertagdescription.{tag}"), + Color = Color.White + }; + } + + playStyleContainer.Recalculate(); + + // ----------------------------------------------------------------------------- + + float elementHeight = 0.075f; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); + + var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; + var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage ?? string.Empty, font: GUIStyle.SmallFont, wrap: true) + { + CanBeFocused = false + }; + serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; + msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); + new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), + TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), + textAlignment: Alignment.Right); + + GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); + new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); + + var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection")); + new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(SubSelectionMode.ToString()), textAlignment: Alignment.Right); + + var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection")); + new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(ModeSelectionMode.ToString()), textAlignment: Alignment.Right); + + if (gameMode.TextSize.X + gameMode.GetChild().TextSize.X > gameMode.Rect.Width || + subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || + modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) + { + gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; + gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; + playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; + } + + var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating")) + { + CanBeFocused = false + }; + allowSpectating.Selected = AllowSpectating; + + var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) + { + CanBeFocused = false + }; + allowRespawn.Selected = AllowRespawn; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) + { + ScrollBarVisible = true, + OnSelected = (component, o) => false + }; + if (ContentPackages.Length == 0) + { + new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + } + else + { + foreach (var package in ContentPackages) + { + var packageText = new GUITickBox( + new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) + { MinSize = new Point(0, 15) }, + package.Name) + { + CanBeFocused = false + }; + if (!string.IsNullOrEmpty(package.Hash)) + { + if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == package.Hash)) + { + packageText.TextColor = GUIStyle.Green; + packageText.Selected = true; + } + //workshop download link found + else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + { + packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); + } + else //no package or workshop download link found + { + packageText.TextColor = GameMain.VanillaContent.NameMatches(package.Name) ? GUIStyle.Red : GUIStyle.Yellow; + packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", + ("[contentpackage]", package.Name), ("[hash]", package.Hash)); + } + } + } + } + + // ----------------------------------------------------------------------------- + + foreach (GUIComponent c in content.Children) + { + if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; } + } + } + + public IEnumerable GetPlayStyleTags() + { + yield return $"Karma.{KarmaEnabled}".ToIdentifier(); + yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); + yield return $"VoIP.{VoipEnabled}".ToIdentifier(); + yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier(); + yield return $"Modded.{ContentPackages.Any()}".ToIdentifier(); + } + + public void UpdateInfo(Func valueGetter) + { + ServerMessage = valueGetter("message") ?? ""; + GameVersion = Version.TryParse(valueGetter("version"), out var version) + ? version + : GameMain.Version; + + if (int.TryParse(valueGetter("playercount"), out int playerCount)) { PlayerCount = playerCount; } + if (int.TryParse(valueGetter("maxplayernum"), out int maxPlayers)) { MaxPlayers = maxPlayers; } + if (Enum.TryParse(valueGetter("modeselectionmode"), out SelectionMode modeSelectionMode)) { ModeSelectionMode = modeSelectionMode; } + if (Enum.TryParse(valueGetter("subselectionmode"), out SelectionMode subSelectionMode)) { SubSelectionMode = subSelectionMode; } + + HasPassword = getBool("haspassword"); + GameStarted = getBool("gamestarted"); + KarmaEnabled = getBool("karmaenabled"); + FriendlyFireEnabled = getBool("friendlyfireenabled"); + AllowSpectating = getBool("allowspectating"); + AllowRespawn = getBool("allowrespawn"); + VoipEnabled = getBool("voicechatenabled"); + + GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; + if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } + if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + + ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); + + bool getBool(string key) + { + string? data = valueGetter(key); + return bool.TryParse(data, out var result) && result; + } + } + + private static ContentPackageInfo[] ExtractContentPackageInfo(Func valueGetter) + { + string? joinedNames = valueGetter("contentpackage"); + string? joinedHashes = valueGetter("contentpackagehash"); + string? joinedWorkshopIds = valueGetter("contentpackageid"); + + string[] contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty() : joinedNames.Split(','); + string[] contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty() : joinedHashes.Split(','); + #warning TODO: genericize + ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); + + if (contentPackageNames.Length != contentPackageHashes.Length + || contentPackageHashes.Length != contentPackageIds.Length) + { + return Array.Empty(); + } + + return contentPackageNames + .Zip(contentPackageHashes, (name, hash) => (name, hash)) + .Zip(contentPackageIds, (t1, id) => + new ContentPackageInfo( + t1.name, + t1.hash, + Option.Some(new SteamWorkshopId(id)))) + .ToArray(); + } + + public static Option FromXElement(XElement element) + { + string endpointStr + = element.GetAttributeString("Endpoint", null) + ?? element.GetAttributeString("OwnerID", null) + ?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}"; + + if (!Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) { return Option.None(); } + + var gameVersionStr = element.GetAttributeString("GameVersion", ""); + if (!Version.TryParse(gameVersionStr, out var gameVersion)) { gameVersion = GameMain.Version; } + var info = new ServerInfo(endpoint) + { + GameVersion = gameVersion + }; + SerializableProperty.DeserializeProperties(info, element); + + info.MetadataSource = DataSource.Parse(element); + + return Option.Some(info); + } + + public XElement ToXElement() + { + XElement element = new XElement(GetType().Name); + + element.SetAttributeValue("Endpoint", Endpoint.ToString()); + element.SetAttributeValue("GameVersion", GameVersion.ToString()); + + SerializableProperty.SerializeProperties(this, element, saveIfDefault: true); + + if (MetadataSource.TryUnwrap(out var dataSource)) + { + dataSource.Write(element); + } + + return element; + } + + public override bool Equals(object? obj) + { + return obj is ServerInfo other && Equals(other); + } + + public bool Equals(ServerInfo other) + => other.Endpoint == Endpoint; + + public override int GetHashCode() => Endpoint.GetHashCode(); + + string ISerializableEntity.Name => "ServerInfo"; + public Dictionary SerializableProperties { get; } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs new file mode 100644 index 000000000..205d4f034 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs @@ -0,0 +1,35 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma +{ + class CompositeServerProvider : ServerProvider + { + private readonly ImmutableArray providers; + + public CompositeServerProvider(params ServerProvider[] providers) + { + this.providers = providers.ToImmutableArray(); + } + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + int providersFinished = 0; + void ackFinishedProvider() + { + providersFinished++; + if (providersFinished == providers.Length) + { + onQueryCompleted(); + } + } + providers.ForEach(p => p.RetrieveServers(onServerDataReceived, ackFinishedProvider)); + } + + public override void Cancel() + => providers.ForEach(p => p.Cancel()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs new file mode 100644 index 000000000..8664c58ed --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs @@ -0,0 +1,17 @@ +#nullable enable +using System; +using Barotrauma.Networking; + +namespace Barotrauma +{ + abstract class ServerProvider + { + public void RetrieveServers(Action onServerDataReceived, Action onQueryCompleted) + { + Cancel(); + RetrieveServersImpl(onServerDataReceived, onQueryCompleted); + } + protected abstract void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted); + public abstract void Cancel(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs new file mode 100644 index 000000000..ff9079caf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -0,0 +1,160 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma +{ + sealed class SteamDedicatedServerProvider : ServerProvider + { + public class DataSource : ServerInfo.DataSource + { + public readonly UInt16 QueryPort; + + public DataSource(UInt16 queryPort) + { + QueryPort = queryPort; + } + + /// Method is invoked via reflection, + /// see + public new static Option Parse(XElement element) + => element.TryGetAttributeInt("QueryPort", out var result) + ? result switch + { + var invalidPort when invalidPort <= 0 || invalidPort > UInt16.MaxValue => Option.None(), + var queryPort => Option.Some(new DataSource((UInt16)queryPort)) + } + : Option.None(); + + public override void Write(XElement element) => element.SetAttributeValue("QueryPort", QueryPort); + } + + private static Option InfoFromListEntry(Steamworks.Data.ServerInfo entry) => + entry.Name.IsNullOrEmpty() + ? Option.None() + : Option.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) + { + ServerName = entry.Name, + HasPassword = entry.Passworded, + PlayerCount = entry.Players, + MaxPlayers = entry.MaxPlayers, + MetadataSource = Option.Some(new DataSource((UInt16)entry.QueryPort)) + }); + + private static void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + { + TaskPool.Add($"QueryServerRules (GetServers, {entry.Name}, {entry.Address})", entry.QueryRulesAsync(), + t => + { + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}"); + return; + } + + if (!t.TryGetResult(out Dictionary rules)) { return; } + if (rules is null) { return; } + if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } + serverInfo.UpdateInfo(key => + { + if (rules.TryGetValue(key, out var val)) { return val; } + return null; + }); + serverInfo.Checked = true; //rules != null; + + onServerDataReceived(serverInfo); + }); + } + + private static void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + { + //TODO: do we still want to list unresponsive servers? + if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } + onServerDataReceived(serverInfo); + } + + private Steamworks.ServerList.Internet? serverQuery; + private CoroutineHandle? queryCoroutine; + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + if (!SteamManager.IsInitialized) + { + onQueryCompleted(); + return; + } + + // All lambdas in here must only capture this call's + // query, not the provider's latest query + var selfServerQuery = new Steamworks.ServerList.Internet(); + serverQuery = selfServerQuery; + + ConcurrentQueue responsiveServers = + new ConcurrentQueue(); + ConcurrentQueue unresponsiveServers = + new ConcurrentQueue(); + + selfServerQuery.OnResponsiveServer = responsiveServers.Enqueue; + selfServerQuery.OnUnresponsiveServer = unresponsiveServers.Enqueue; + + void dequeue(int? limit = null) + { + for (int i = 0; (!limit.HasValue || i < limit) && responsiveServers.TryDequeue(out var serverInfo); i++) + { + HandleResponsiveServer(serverInfo, onServerDataReceived); + } + + for (int i = 0; (!limit.HasValue || i < limit) && unresponsiveServers.TryDequeue(out var serverInfo); i++) + { + HandleUnresponsiveServer(serverInfo, onServerDataReceived); + } + } + + IEnumerable dequeueCoroutine() + { + while (true) + { + dequeue(limit: 20); + yield return new WaitForSeconds(0.1f, ignorePause: true); + } + } + var selfQueryCoroutine = CoroutineManager.StartCoroutine(dequeueCoroutine(), + $"{nameof(SteamDedicatedServerProvider)}.{nameof(RetrieveServers)}.{nameof(dequeueCoroutine)}"); + queryCoroutine = selfQueryCoroutine; + + TaskPool.Add("RunServerQuery", selfServerQuery.RunQueryAsync(timeoutSeconds: 30f), + t => + { + try + { + // Clear the callbacks because it's too late now, we want to get this over with + selfServerQuery.OnResponsiveServer = null; + selfServerQuery.OnUnresponsiveServer = null; + + CoroutineManager.StopCoroutines(selfQueryCoroutine); + dequeue(); + + if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); } + + selfServerQuery.Dispose(); + } + finally + { + onQueryCompleted(); + } + }); + } + + public override void Cancel() + { + if (queryCoroutine != null) { CoroutineManager.StopCoroutines(queryCoroutine); } + serverQuery?.Dispose(); + serverQuery = null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs new file mode 100644 index 000000000..fe80749e5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma +{ + sealed class SteamP2PServerProvider : ServerProvider + { + public class DataSource : ServerInfo.DataSource + { + public readonly Steamworks.Data.Lobby Lobby; + + public override void Write(XElement element) { /* do nothing */ } + + public DataSource(Steamworks.Data.Lobby lobby) + { + Lobby = lobby; + } + } + + private object? queryRef = null; + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + if (!SteamManager.IsInitialized) + { + onQueryCompleted(); + return; + } + + // All lambdas and local methods in here must only capture + // this call's query, not the provider's latest query + var selfQueryRef = new object(); + queryRef = selfQueryRef; + + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() + .FilterDistanceWorldwide() + .WithMaxResults(50); + // Steamworks is unable to retrieve more than 50 lobbies per request + // (see https://partner.steamgames.com/doc/features/multiplayer/matchmaking#3) + // To work around this, we'll make up to 10 requests, asking to ignore + // all previous results in each subsequent request. + #warning TODO: do something less horrible here? + + int requestCount = 0; + HashSet retrieved = new HashSet(); + + void startQuery() + { + if (requestCount >= 10) { return; } + requestCount++; + TaskPool.Add($"LobbyQuery.RequestAsync ({requestCount})", lobbyQuery.RequestAsync(), onRequestComplete); + } + + void onRequestComplete(Task t) + { + // If queryRef != selfQueryRef, this query was cancelled + if (!ReferenceEquals(selfQueryRef, queryRef)) { return; } + + if (!t.TryGetResult(out Steamworks.Data.Lobby[] lobbies) + || lobbies is null + || lobbies.Length == 0) + { + onQueryCompleted(); + return; + } + + foreach (var lobby in lobbies) + { + string lobbyOwnerStr = lobby.GetData("lobbyowner"); + lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); + + string serverName = lobby.GetData("name"); + if (string.IsNullOrEmpty(serverName)) { continue; } + + var ownerId = SteamId.Parse(lobbyOwnerStr); + if (!ownerId.TryUnwrap(out var lobbyOwnerId)) { continue; } + + if (retrieved.Contains(lobbyOwnerId)) { continue; } + retrieved.Add(lobbyOwnerId); + + var serverInfo = new ServerInfo(new SteamP2PEndpoint(lobbyOwnerId)) + { + ServerName = serverName, + MetadataSource = Option.Some(new DataSource(lobby)) + }; + serverInfo.UpdateInfo(key => lobby.GetData(key)); + serverInfo.Checked = true; + + onServerDataReceived(serverInfo); + } + startQuery(); + } + + startQuery(); + } + + public override void Cancel() + { + queryRef = null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 018a897a3..10829d4fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -125,8 +125,6 @@ namespace Barotrauma.Networking public void ClientRead(IReadMessage incMsg) { - cachedServerListInfo = null; - NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); if (requiredFlags.HasFlag(NetFlags.Name)) @@ -146,7 +144,6 @@ namespace Barotrauma.Networking AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); - GameMain.NetworkMember.TickRate = TickRate; if (requiredFlags.HasFlag(NetFlags.Properties)) { @@ -241,7 +238,8 @@ namespace Barotrauma.Networking outMsg.WriteSingle(levelDifficulty ?? -1000.0f); - outMsg.WriteBoolean(useRespawnShuttle ?? UseRespawnShuttle); + outMsg.WriteBoolean(useRespawnShuttle != null); + outMsg.WriteBoolean(useRespawnShuttle ?? false); outMsg.WriteBoolean(autoRestart != null); outMsg.WriteBoolean(autoRestart ?? false); @@ -1057,15 +1055,7 @@ namespace Barotrauma.Networking } settingsFrame = null; } - return false; } - - private ServerInfo cachedServerListInfo = null; - public ServerInfo GetServerListInfo() - { - cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this); - return cachedServerListInfo; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index b2f044280..f07d6f7a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using System; namespace Barotrauma { @@ -111,6 +112,23 @@ namespace Barotrauma } } + public void ResetVotes(IEnumerable connectedClients) + { + foreach (Client client in connectedClients) + { + client.ResetVotes(); + } + + foreach (VoteType voteType in Enum.GetValues(typeof(VoteType))) + { + SetVoteCountYes(voteType, 0); + SetVoteCountNo(voteType, 0); + SetVoteCountMax(voteType, 0); + } + UpdateVoteTexts(connectedClients, VoteType.Mode); + UpdateVoteTexts(connectedClients, VoteType.Sub); + } + public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.WriteByte((byte)voteType); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0f1ec526f..01f4b8506 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -67,6 +67,8 @@ namespace Barotrauma public static readonly Queue WorkshopItemsToUpdate = new Queue(); + private readonly GUIListBox tutorialList; + #region Creation public MainMenuScreen(GameMain game) { @@ -429,26 +431,19 @@ namespace Barotrauma menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); //PLACEHOLDER - var tutorialList = new GUIListBox( + tutorialList = new GUIListBox( new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) { PlaySoundOnSelect = true, }; - foreach (var tutorialPrefab in TutorialPrefab.Prefabs.OrderBy(p => p.Order)) - { - var tutorial = new Tutorial(tutorialPrefab); - var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont) - { - TextColor = GUIStyle.Green, - UserData = tutorial - }; - } tutorialList.OnSelected += (component, obj) => { (obj as Tutorial)?.Start(); return true; }; + CreateTutorialButtons(); + this.game = game; menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) @@ -463,7 +458,28 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); } -#endregion + + private void CreateTutorialButtons() + { + foreach (var tutorialPrefab in TutorialPrefab.Prefabs.OrderBy(p => p.Order)) + { + var tutorial = new Tutorial(tutorialPrefab); + var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont) + { + TextColor = GUIStyle.Green, + UserData = tutorial + }; + } + } + + public static void UpdateInstanceTutorialButtons() + { + if (GameMain.MainMenuScreen is not MainMenuScreen menuScreen) { return; } + menuScreen.tutorialList.ClearChildren(); + menuScreen.CreateTutorialButtons(); + } + + #endregion #region Selection public override void Select() @@ -1117,7 +1133,7 @@ namespace Barotrauma var playstyleContainer = new GUIFrame(new RectTransform(new Vector2(1.35f, 0.1f), parent.RectTransform), style: null, color: Color.Black); playstyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 0.1f), playstyleContainer.RectTransform), - ServerListScreen.PlayStyleBanners[0], scaleToFit: true) + GUIStyle.GetComponentStyle($"PlayStyleBanner.{PlayStyle.Serious}").GetSprite(GUIComponent.ComponentState.None), scaleToFit: true) { UserData = PlayStyle.Serious }; @@ -1336,12 +1352,15 @@ namespace Barotrauma private void SetServerPlayStyle(PlayStyle playStyle) { - playstyleBanner.Sprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; + playstyleBanner.Sprite = GUIStyle + .GetComponentStyle($"PlayStyleBanner.{playStyle}") + .GetSprite(GUIComponent.ComponentState.None); playstyleBanner.UserData = playStyle; var nameText = playstyleBanner.GetChild(); nameText.Text = TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag." + playStyle)); - nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.Color = playstyleBanner.Sprite + .SourceElement.GetAttributeColor("BannerColor") ?? Color.White; nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index e638bf0e5..a37fda6f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -92,7 +92,7 @@ namespace Barotrauma var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages .Where(sp => sp.ContentPackage is null).ToArray(); - if (!missingPackages.Any()) + if (!missingPackages.Any(p => p.IsMandatory)) { if (!GameMain.Client.IsServerOwner) { @@ -106,7 +106,7 @@ namespace Barotrauma .Select(p => p.RegularPackage) .OfType().ToList(); //keep enabled client-side-only mods enabled - regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent)); + regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p))); ContentPackageManager.EnabledPackages.SetRegular(regularPackages); } GameMain.NetLobbyScreen.Select(); @@ -177,11 +177,11 @@ namespace Barotrauma }); buttonContainerSpacing(0.1f); - var missingIds = missingPackages.Where( - mp => mp.WorkshopId != 0 - && ContentPackageManager.WorkshopPackages.All(wp - => wp.SteamWorkshopId != mp.WorkshopId)) - .Select(mp => mp.WorkshopId) + var missingIds = missingPackages + .Where(p => p.IsMandatory) + .Select(mp => ContentPackageId.Parse(mp.UgcId)) + .NotNone() + .Where(id => ContentPackageManager.WorkshopPackages.All(wp => !wp.UgcId.Equals(id))) .ToArray(); if (missingIds.Any() && SteamManager.IsInitialized) { @@ -191,7 +191,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - BulkDownloader.SubscribeToServerMods(missingIds, + BulkDownloader.SubscribeToServerMods(missingIds.OfType().Select(id => id.Value), new ConnectCommand( serverName: GameMain.Client.ServerName, endpoint: GameMain.Client.ClientPeer.ServerEndpoint)); @@ -202,7 +202,7 @@ namespace Barotrauma buttonContainerSpacing(0.15f); } - foreach (var p in missingPackages) + foreach (var p in missingPackages.Where(p => p.IsMandatory)) { pendingDownloads.Enqueue(p); @@ -294,26 +294,50 @@ namespace Barotrauma ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) ?.CorePackage ?? throw new Exception($"Failed to find core package to enable"); - 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() - .ToList(); + + List regularPackages = new List(); + foreach (var p in serverPackages) + { + if (p.CorePackage != null) { continue; } + RegularPackage? matchingPackage = + p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage; + if (matchingPackage is null) + { + if (!p.IsMandatory) + { + //we don't need to care about missing non-mandatory (= submarine) mods + continue; + } + else + { + throw new Exception($"Could not find regular package \"{p.Name}\""); + } + } + regularPackages.Add(matchingPackage); + } foreach (var regularPackage in regularPackages) { DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime); } //keep enabled client-side-only mods enabled - regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent)); + regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p))); ContentPackageManager.EnabledPackages.BackUp(); ContentPackageManager.EnabledPackages.SetCore(corePackage); ContentPackageManager.EnabledPackages.SetRegular(regularPackages); + //see if any of the packages we enabled contain subs that we were missing previously, and update their paths + foreach (var serverSub in GameMain.Client.ServerSubmarines) + { + if (File.Exists(serverSub.FilePath)) { continue; } + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSub.Name && s.MD5Hash == serverSub.MD5Hash); + if (matchingSub != null) + { + serverSub.FilePath = matchingSub.FilePath; + } + } + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, GameMain.Client.ServerSubmarines); GameMain.NetLobbyScreen.Select(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 9c9e999d6..9471d8128 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -666,7 +666,7 @@ namespace Barotrauma OnSelected = (tickbox) => { if (GameMain.Client == null) { return true; } - ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); + ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); if (tickbox.Selected) { GameMain.ServerListScreen.AddToFavoriteServers(info); @@ -1432,10 +1432,6 @@ namespace Barotrauma bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText = null; - if (isGameRunning) - { - infoContainer.RectTransform.AbsoluteOffset = new Point(0, (int)(parent.Rect.Height * 0.025f)); - } if (TabMenu.PendingChanges) { @@ -1454,7 +1450,6 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } string newName = Client.SanitizeName(tb.Text); - newName = newName.Replace(":", "").Replace(";", ""); if (newName == GameMain.Client.Name) return; if (string.IsNullOrWhiteSpace(newName)) { @@ -1782,6 +1777,10 @@ namespace Barotrauma // Hide spectate tickbox if spectating is not allowed spectateBox.Visible = allowSpectating; + if (infoContainer != null) + { + infoContainer.RectTransform.RelativeSize = new Vector2(infoContainer.RectTransform.RelativeSize.X, spectateBox.Visible ? 0.92f : 0.97f); + } } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -2753,25 +2752,24 @@ namespace Barotrauma if (GameMain.NetworkMember?.ServerSettings == null) { return; } PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; - if ((int)playStyle < 0 || - (int)playStyle >= ServerListScreen.PlayStyleBanners.Length) - { - return; - } - Sprite sprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; + Sprite sprite = GUIStyle + .GetComponentStyle($"PlayStyleBanner.{playStyle}")? + .GetSprite(GUIComponent.ComponentState.None); + if (sprite is null) { return; } + float scale = component.Rect.Width / sprite.size.X; sprite.Draw(spriteBatch, component.Center, scale: scale); if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) { var nameText = component.GetChild(); - nameText.Text = TextManager.Get("servertag." + playStyle); - nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.Text = TextManager.Get($"ServerTag.{playStyle}"); + nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); prevPlayStyle = playStyle; - component.ToolTip = TextManager.Get("servertagdescription." + playStyle); + component.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); } publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs deleted file mode 100644 index 984cb5cbb..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ /dev/null @@ -1,2044 +0,0 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Barotrauma.Networking; -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Barotrauma -{ - class ServerListScreen : Screen - { - //how often the client is allowed to refresh servers - private readonly TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); - - public ImmutableDictionary ContentPackagesByWorkshopId { get; private set; } - = ImmutableDictionary.Empty; - public ImmutableDictionary ContentPackagesByHash { get; private set; } - = ImmutableDictionary.Empty; - - private GUIFrame menu; - - private GUIListBox serverList; - private GUIFrame serverPreviewContainer; - private GUIListBox serverPreview; - - private GUIButton joinButton; - private ServerInfo selectedServer; - - private GUIButton scanServersButton; - - //friends list - private GUILayoutGroup friendsButtonHolder; - - private GUIButton friendsDropdownButton; - private GUIListBox friendsDropdown; - - private enum TernaryOption - { - Any, - Enabled, - Disabled - } - - private class FriendInfo - { - public UInt64 SteamId; - public string Name; - public Sprite Sprite; - public LocalizedString StatusText; - public bool PlayingThisGame; - public bool PlayingAnotherGame; - public Option ConnectCommand = Option.None(); - - public bool InServer - { - get - { - return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && ConnectCommand.IsSome(); - } - } - } - private List friendsList; - private GUIFrame friendPopup; - private double friendsListUpdateTime; - - //favorite servers/history - private const string recentServersFile = "Data/recentservers.xml"; - private const string favoriteServersFile = "Data/favoriteservers.xml"; - private List favoriteServers; - private List recentServers; - - private readonly Dictionary activePings = new Dictionary(); - - private enum ServerListTab - { - All = 0, - Favorites = 1, - Recent = 2 - }; - private ServerListTab selectedTab; - private ServerListTab SelectedTab - { - get { return selectedTab; } - set - { - if (selectedTab == value) { return; } - var tabVals = Enum.GetValues(typeof(ServerListTab)); - for (int i = 0; i < tabVals.Length; i++) - { - tabButtons[i].Selected = false; - } - tabButtons[(int)value].Selected = true; - selectedTab = value; - FilterServers(); - } - } - private GUIButton[] tabButtons; - - private static Sprite[] playStyleBanners; - //server playstyle and tags - public static Sprite[] PlayStyleBanners - { - get - { - if (playStyleBanners == null) - { - LoadPlayStyleBanners(); - } - return playStyleBanners; - } - } - public static Color[] PlayStyleColors - { - get; private set; - } - - public GUITextBox ClientNameBox { get; private set; } - - public static Dictionary PlayStyleIcons - { - get; private set; - } - public static Dictionary PlayStyleIconColors - { - get; private set; - } - - private bool masterServerResponded; - private IRestResponse masterServerResponse; - - private readonly float[] columnRelativeWidth = new float[] { 0.1f, 0.1f, 0.7f, 0.12f, 0.08f, 0.08f }; - private readonly string[] columnLabel = new string[] { "ServerListCompatible", "ServerListHasPassword", "ServerListName", "ServerListRoundStarted", "ServerListPlayers", "ServerListPing" }; - - private GUILayoutGroup labelHolder; - private readonly List labelTexts = new List(); - - //filters - private GUITextBox searchBox; - private GUITickBox filterSameVersion; - private GUITickBox filterPassword; - private GUITickBox filterIncompatible; - private GUITickBox filterFull; - private GUITickBox filterEmpty; - private Dictionary ternaryFilters; - private Dictionary filterTickBoxes; - private Dictionary playStyleTickBoxes; - private Dictionary gameModeTickBoxes; - private GUITickBox filterOffensive; - - //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. - private TernaryOption filterFriendlyFireValue = TernaryOption.Any; - private TernaryOption filterKarmaValue = TernaryOption.Any; - private TernaryOption filterTraitorValue = TernaryOption.Any; - private TernaryOption filterVoipValue = TernaryOption.Any; - private TernaryOption filterModdedValue = TernaryOption.Any; - - private string sortedBy; - - private GUIButton serverPreviewToggleButton; - - //a timer for preventing the client from spamming the refresh button faster than AllowedRefreshInterval - private DateTime refreshDisableTimer; - private bool waitingForRefresh; - - private bool steamPingInfoReady; - - private const float sidebarWidth = 0.2f; - public ServerListScreen() - { - GameMain.Instance.ResolutionChanged += CreateUI; - CreateUI(); - } - - private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) - { - var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) - { - Stretch = true - }; - - var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) - { - IsFixedSize = true, - }, null) - { - HoverColor = Color.Gray, - SelectedColor = Color.DarkGray, - CanBeFocused = false - }; - if (box.RectTransform.MinSize.Y > 0) - { - box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); - box.RectTransform.Resize(box.RectTransform.MinSize); - } - Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); - - var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) - { - UserData = TextManager.Get("servertag." + tag + ".label") - }; - GUIStyle.Apply(filterLabel, "GUITextBlock", null); - - var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); - dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); - dropDown.AddItem(TextManager.Get("servertag." + tag + ".true"), TernaryOption.Enabled, TextManager.Get("servertagdescription." + tag + ".true")); - dropDown.AddItem(TextManager.Get("servertag." + tag + ".false"), TernaryOption.Disabled, TextManager.Get("servertagdescription." + tag + ".false")); - dropDown.SelectItem(TernaryOption.Any); - dropDown.OnSelected = (_, data) => { - valueSetter((TernaryOption)data); - FilterServers(); - StoreServerFilters(); - return true; - }; - - ternaryFilters.Add(tag, dropDown); - } - - private void CreateUI() - { - menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); - - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center)) - { - RelativeSpacing = 0.02f, - Stretch = true - }; - - //------------------------------------------------------------------------------------- - //Top row - //------------------------------------------------------------------------------------- - - var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) - { - Padding = Vector4.Zero, - ForceUpperCase = ForceUpperCase.Yes, - AutoScaleHorizontal = true - }; - - var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; - - var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); - ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") - { - Text = MultiplayerPreferences.Instance.PlayerName, - MaxTextLength = Client.MaxNameLength, - OverflowClip = true - }; - - if (string.IsNullOrEmpty(ClientNameBox.Text)) - { - ClientNameBox.Text = SteamManager.GetUsername(); - } - ClientNameBox.OnTextChanged += (textbox, text) => - { - MultiplayerPreferences.Instance.PlayerName = text; - return true; - }; - - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); - - var tabVals = Enum.GetValues(typeof(ServerListTab)); - tabButtons = new GUIButton[tabVals.Length]; - foreach (ServerListTab tab in tabVals) - { - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabButtonHolder.RectTransform), - TextManager.Get("ServerListTab." + tab.ToString()), style: "GUITabButton") - { - OnClicked = (btn, usrdat) => - { - SelectedTab = tab; - return false; - } - }; - } - - var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") - { - IgnoreLayoutGroups = true - }; - - friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; - friendsList = new List(); - - //------------------------------------------------------------------------------------- - // Bottom row - //------------------------------------------------------------------------------------- - - var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y), - paddedFrame.RectTransform, Anchor.CenterRight)) - { - Stretch = true - }; - - var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - OutlineColor = Color.Black - }; - - GUILayoutGroup serverListContainer = null; - GUIFrame filtersHolder = null; - GUIButton filterToggle = null; - - void RecalculateHolder() - { - float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; - listContainerSubtract += serverPreviewContainer.Visible ? sidebarWidth : 0.0f; - - float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; - listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; - listContainerSubtract += serverPreviewContainer.Visible ? toggleButtonsSubtract : 0.0f; - - serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); - serverListHolder.Recalculate(); - } - - // filters ------------------------------------------- - - filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) - { - Color = new Color(12, 14, 15, 255) * 0.5f, - OutlineColor = Color.Black - }; - - float elementHeight = 0.05f; - var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) - { - Padding = Vector4.Zero, - AutoScaleHorizontal = true, - CanBeFocused = false - }; - - var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true }; - - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); - searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; - - var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft)) - { - ScrollBarVisible = true, - Spacing = (int)(5 * GUI.Scale) - }; - - filterToggle = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) - { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, - style: "UIToggleButton") - { - OnClicked = (btn, userdata) => - { - filtersHolder.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); - filtersHolder.Visible = !filtersHolder.Visible; - filtersHolder.IgnoreLayoutGroups = !filtersHolder.Visible; - - RecalculateHolder(); - - btn.Children.ForEach(c => c.SpriteEffects = !filtersHolder.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - return true; - } - }; - filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - - ternaryFilters = new Dictionary(); - filterTickBoxes = new Dictionary(); - - GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) - { - text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) - { - UserData = text, - Selected = defaultState, - ToolTip = addTooltip ? text : null, - OnSelected = (tickBox) => - { - FilterServers(); - StoreServerFilters(); - return true; - } - }; - filterTickBoxes.Add(key, tickBox); - return tickBox; - } - - filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); - filterPassword = addTickBox("FilterPassword".ToIdentifier()); - filterIncompatible = addTickBox("FilterIncompatibleServers".ToIdentifier()); - filterFull = addTickBox("FilterFullServers".ToIdentifier()); - filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); - filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); - - // Filter Tags - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) - { - CanBeFocused = false - }; - - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); - - // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) - { - CanBeFocused = false - }; - - playStyleTickBoxes = new Dictionary(); - foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) - { - var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); - selectionTick.UserData = playStyle; - playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); - } - - // Game mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - - gameModeTickBoxes = new Dictionary(); - foreach (GameModePreset mode in GameModePreset.List) - { - if (mode.IsSinglePlayer) { continue; } - - var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); - selectionTick.UserData = mode.Identifier; - gameModeTickBoxes.Add(mode.Identifier, selectionTick); - } - - filters.Content.RectTransform.SizeChanged += () => - { - filters.Content.RectTransform.RecalculateChildren(true, true); - filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); - gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); - playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); - GUITextBlock.AutoScaleAndNormalize( - filterTickBoxes.Values.Select(tb => tb.TextBlock) - .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), - defaultScale: 1.0f); - if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) - { - filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); - filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f))); - } - }; - - // server list --------------------------------------------------------------------- - - serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; - - labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, - isHorizontal: true, childAnchor: Anchor.BottomLeft) - { - Stretch = true - }; - - for (int i = 0; i < columnRelativeWidth.Length; i++) - { - var btn = new GUIButton(new RectTransform(new Vector2(columnRelativeWidth[i], 1.0f), labelHolder.RectTransform), - text: TextManager.Get(columnLabel[i]), textAlignment: Alignment.Center, style: "GUIButtonSmall") - { - ToolTip = TextManager.Get(columnLabel[i]), - ForceUpperCase = ForceUpperCase.Yes, - UserData = columnLabel[i], - OnClicked = SortList - }; - btn.Color *= 0.5f; - labelTexts.Add(btn.TextBlock); - - new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) - { - CanBeFocused = false, - UserData = "arrowup", - Visible = false - }; - new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) - { - CanBeFocused = false, - UserData = "arrowdown", - SpriteEffects = SpriteEffects.FlipVertically, - Visible = false - }; - } - - serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) - { - PlaySoundOnSelect = true, - ScrollBarVisible = true, - OnSelected = (btn, obj) => - { - if (obj is ServerInfo serverInfo) - { - joinButton.Enabled = true; - selectedServer = serverInfo; - if (!serverPreviewContainer.Visible) - { - serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); - serverPreviewToggleButton.Visible = true; - serverPreviewToggleButton.IgnoreLayoutGroups = false; - serverPreviewContainer.Visible = true; - serverPreviewContainer.IgnoreLayoutGroups = false; - RecalculateHolder(); - } - serverInfo.CreatePreviewWindow(serverPreview.Content); - serverPreview.ForceLayoutRecalculation(); - btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - } - return true; - } - }; - - //server preview panel -------------------------------------------------- - - serverPreviewToggleButton = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) - { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, - style: "UIToggleButton") - { - Visible = false, - OnClicked = (btn, userdata) => - { - serverPreviewContainer.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); - serverPreviewContainer.Visible = !serverPreviewContainer.Visible; - serverPreviewContainer.IgnoreLayoutGroups = !serverPreviewContainer.Visible; - - RecalculateHolder(); - - btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); - return true; - } - }; - - serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) - { - Color = new Color(12, 14, 15, 255) * 0.5f, - OutlineColor = Color.Black, - IgnoreLayoutGroups = true, - Visible = false - }; - serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) - { - Padding = Vector4.One * 10 * GUI.Scale, - HoverCursor = CursorState.Default, - OnSelected = (component, o) => false - }; - - // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.02f, - Stretch = true - }; - - GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Back")) - { - OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu - }; - - scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("ServerListRefresh")) - { - OnClicked = (btn, userdata) => { RefreshServers(); return true; } - }; - - var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("serverlistdirectjoin")) - { - OnClicked = (btn, userdata) => - { - if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) - { - ClientNameBox.Flash(); - ClientNameBox.Select(); - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - return false; - } - ShowDirectJoinPrompt(); - return true; - } - }; - - joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("ServerListJoin")) - { - OnClicked = (btn, userdata) => - { - if (selectedServer != null) - { - if (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")); - return false; - } - } - return true; - }, - Enabled = false - }; - - buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f)); - - //-------------------------------------------------------- - - bottomRow.Recalculate(); - serverListHolder.Recalculate(); - serverListContainer.Recalculate(); - labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); - labelHolder.Recalculate(); - - serverList.Content.RectTransform.SizeChanged += () => - { - labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); - labelHolder.Recalculate(); - foreach (GUITextBlock labelText in labelTexts) - { - labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); - } - RecalculateHolder(); - }; - - button.SelectedColor = button.Color; - refreshDisableTimer = DateTime.Now; - - //recent and favorite servers - ReadServerMemFromFile(recentServersFile, ref recentServers); - ReadServerMemFromFile(favoriteServersFile, ref favoriteServers); - recentServers.ForEach(s => s.Recent = true); - favoriteServers.ForEach(s => s.Favorite = true); - - SelectedTab = ServerListTab.All; - tabButtons[(int)selectedTab].Selected = true; - - RecalculateHolder(); - } - - - private static void LoadPlayStyleBanners() - { - //playstyle banners - playStyleBanners = new Sprite[Enum.GetValues(typeof(PlayStyle)).Length]; - PlayStyleColors = new Color[Enum.GetValues(typeof(PlayStyle)).Length]; - PlayStyleIcons = new Dictionary(); - PlayStyleIconColors = new Dictionary(); - - XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); - - var rootElement = playStylesDoc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); - foreach (var element in rootElement.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "playstylebanner": - if (Enum.TryParse(element.GetAttributeString("identifier", ""), out PlayStyle playStyle)) - { - PlayStyleBanners[(int)playStyle] = new Sprite(element, lazyLoad: true); - PlayStyleColors[(int)playStyle] = element.GetAttributeColor("color", Color.White); - } - break; - case "playstyleicon": - string identifier = element.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(identifier)) { continue; } - PlayStyleIcons[identifier] = new Sprite(element, lazyLoad: true); - PlayStyleIconColors[identifier] = element.GetAttributeColor("color", Color.White); - break; - } - } - } - - private void ReadServerMemFromFile(string file, ref List servers) - { - if (servers == null) { servers = new List(); } - servers.Clear(); - - if (!File.Exists(file)) { return; } - - XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null) - { - DebugConsole.NewMessage("Failed to load file \"" + file + "\". Attempting to recreate the file..."); - try - { - doc = new XDocument(new XElement("servers")); - doc.Save(file); - DebugConsole.NewMessage("Recreated \"" + file + "\"."); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to recreate the file \"" + file + "\".", e); - } - return; - } - - bool saveCleanup = false; - foreach (XElement element in doc.Root.Elements()) - { - if (element.Name != "ServerInfo") { continue; } - var info = ServerInfo.FromXElement(element); - if (!servers.Any(s => s.Equals(info))) - { - servers.Add(info); - } - else - { - saveCleanup = true; - } - } - if (saveCleanup) { WriteServerMemToFile(file, servers); } - } - - private void WriteServerMemToFile(string file, List servers) - { - if (servers == null) { return; } - - XDocument doc = new XDocument(); - XElement rootElement = new XElement("servers"); - doc.Add(rootElement); - - foreach (ServerInfo info in servers) - { - rootElement.Add(info.ToXElement()); - } - - doc.SaveSafe(file); - } - - public ServerInfo UpdateServerInfoWithServerSettings(NetworkConnection connection, ServerSettings serverSettings) - { - bool isInfoNew = false; - ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && - serverInfo.Endpoint == connection.Endpoint)?.UserData as ServerInfo; - if (info == null) - { - isInfoNew = true; - info = new ServerInfo(); - } - - info.ServerName = serverSettings.ServerName; - info.ServerMessage = serverSettings.ServerMessageText; - info.Endpoint = connection.Endpoint; - info.LobbyID = SteamManager.CurrentLobbyID; - 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.TraitorsEnabled = serverSettings.TraitorsEnabled; - info.SubSelectionMode = serverSettings.SubSelectionMode; - info.ModeSelectionMode = serverSettings.ModeSelectionMode; - info.VoipEnabled = serverSettings.VoiceChatEnabled; - info.FriendlyFireEnabled = serverSettings.AllowFriendlyFire; - info.KarmaEnabled = serverSettings.KarmaEnabled; - info.PlayerCount = GameMain.Client.ConnectedClients.Count; - info.PingChecked = false; - info.HasPassword = serverSettings.HasPassword; - info.OwnerVerified = true; - - if (isInfoNew) - { - AddToServerList(info); - } - - return info; - } - - public void AddToRecentServers(ServerInfo info) - { - if (info.Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ip } } && IPAddress.IsLoopback(ip)) - { - return; - } - - info.Recent = true; - ServerInfo existingInfo = recentServers.Find(info.MatchesByEndpoint); - if (existingInfo == null) - { - recentServers.Add(info); - } - else - { - int index = recentServers.IndexOf(existingInfo); - recentServers[index] = info; - } - - WriteServerMemToFile(recentServersFile, recentServers); - } - - public bool IsFavorite(ServerInfo info) - { - return favoriteServers.Any(info.MatchesByEndpoint); - } - - public void AddToFavoriteServers(ServerInfo info) - { - info.Favorite = true; - ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); - if (existingInfo == null) - { - favoriteServers.Add(info); - } - else - { - int index = favoriteServers.IndexOf(existingInfo); - favoriteServers[index] = info; - } - - WriteServerMemToFile(favoriteServersFile, favoriteServers); - } - - public void RemoveFromFavoriteServers(ServerInfo info) - { - info.Favorite = false; - ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); - if (existingInfo != null) - { - favoriteServers.Remove(existingInfo); - WriteServerMemToFile(favoriteServersFile, favoriteServers); - } - } - - private bool SortList(GUIButton button, object obj) - { - if (!(obj is string sortBy)) { return false; } - SortList(sortBy, toggle: true); - return true; - } - - private void SortList(string sortBy, bool toggle) - { - if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } - - sortedBy = sortBy; - - var arrowUp = button.GetChildByUserData("arrowup"); - var arrowDown = button.GetChildByUserData("arrowdown"); - - //disable arrow buttons in other labels - foreach (var child in button.Parent.Children) - { - if (child != button) - { - child.GetChildByUserData("arrowup").Visible = false; - child.GetChildByUserData("arrowdown").Visible = false; - } - } - - bool ascending = arrowUp.Visible; - if (toggle) - { - ascending = !ascending; - } - - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; - serverList.Content.RectTransform.SortChildren((c1, c2) => - { - ServerInfo s1 = c1.GUIComponent.UserData as ServerInfo; - ServerInfo s2 = c2.GUIComponent.UserData as ServerInfo; - - if (s1 == null && s2 == null) - { - return 0; - } - else if (s1 == null) - { - return ascending ? 1 : -1; - } - else if (s2 == null) - { - return ascending ? -1 : 1; - } - - switch (sortBy) - { - case "ServerListCompatible": - bool? s1Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s1.GameVersion); - bool? s2Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s2.GameVersion); - - //convert to int to make sorting easier - //1 Compatible - //0 Unknown - //-1 Incompatible - int s1CompatibleInt = s1Compatible.HasValue ? - (s1Compatible.Value ? 1 : -1) : - 0; - int s2CompatibleInt = s2Compatible.HasValue ? - (s2Compatible.Value ? 1 : -1) : - 0; - return s2CompatibleInt.CompareTo(s1CompatibleInt) * (ascending ? 1 : -1); - case "ServerListHasPassword": - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case "ServerListName": - return string.Compare(s1.ServerName, s2.ServerName) * (ascending ? 1 : -1); - case "ServerListRoundStarted": - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case "ServerListPlayers": - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case "ServerListPing": - return s2.Ping.CompareTo(s1.Ping) * (ascending ? 1 : -1); - default: - return 0; - } - }); - } - - public override void Select() - { - base.Select(); - - ContentPackagesByWorkshopId = ContentPackageManager.AllPackages - .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) - .Where(p => p.Key != 0) - .GroupBy(x => x.Key).Select(g => g.First()) - .ToImmutableDictionary(); - ContentPackagesByHash = ContentPackageManager.AllPackages - .Select(p => new KeyValuePair(p.Hash.StringRepresentation, p)) - .GroupBy(x => x.Key).Select(g => g.First()) - .ToImmutableDictionary(); - - SelectedTab = ServerListTab.All; - GameMain.ServerListScreen.LoadServerFilters(); - if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) - { - var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => - { - filterOffensive.Selected = true; - filterOffensivePrompt.Close(); - return true; - }; - filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; - - var config = GameSettings.CurrentConfig; - config.ShowOffensiveServerPrompt = false; - GameSettings.SetCurrentConfig(config); - } - - Steamworks.SteamMatchmaking.ResetActions(); - - if (GameMain.Client != null) - { - GameMain.Client.Quit(); - GameMain.Client = null; - } - - RefreshServers(); - } - - public override void Deselect() - { - ContentPackagesByWorkshopId = ImmutableDictionary.Empty; - ContentPackagesByHash = ImmutableDictionary.Empty; - base.Deselect(); - - GameSettings.SaveCurrentConfig(); - } - - public override void Update(double deltaTime) - { - base.Update(deltaTime); - - UpdateFriendsList(); - UpdateInfoQueries(); - - if (PlayerInput.PrimaryMouseButtonClicked()) - { - friendPopup = null; - if (friendsDropdown != null && friendsDropdownButton != null && - !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && - !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) - { - friendsDropdown.Visible = false; - } - } - } - - private void FilterServers() - { - serverList.Content.RemoveChild(serverList.Content.FindChild("noresults")); - - foreach (GUIComponent child in serverList.Content.Children) - { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } - - Version remoteVersion = null; - if (!string.IsNullOrEmpty(serverInfo.GameVersion)) - { - Version.TryParse(serverInfo.GameVersion, out remoteVersion); - } - - //never show newer versions - //(ignore revision number, it doesn't affect compatibility) - if (remoteVersion != null && - ToolBox.VersionNewerIgnoreRevision(GameMain.Version, remoteVersion)) - { - child.Visible = false; - } - else - { - bool incompatible = - remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion); - - var karmaFilterPassed = filterKarmaValue == TernaryOption.Any|| (filterKarmaValue == TernaryOption.Enabled) == serverInfo.KarmaEnabled; - var friendlyFireFilterPassed = filterFriendlyFireValue == TernaryOption.Any || (filterFriendlyFireValue == TernaryOption.Enabled) == serverInfo.FriendlyFireEnabled; - var traitorsFilterPassed = filterTraitorValue == TernaryOption.Any || (filterTraitorValue == TernaryOption.Enabled) == (serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe); - var voipFilterPassed = filterVoipValue == TernaryOption.Any || (filterVoipValue == TernaryOption.Enabled) == serverInfo.VoipEnabled; - var moddedFilterPassed = filterModdedValue == TernaryOption.Any || (filterModdedValue == TernaryOption.Enabled) == serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true")); - - child.Visible = - serverInfo.OwnerVerified && - serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && - (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && - (!filterPassword.Selected || !serverInfo.HasPassword) && - (!filterIncompatible.Selected || !incompatible) && - (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && - (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && - (!filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && - karmaFilterPassed && - friendlyFireFilterPassed && - traitorsFilterPassed && - voipFilterPassed && - moddedFilterPassed && - ((selectedTab == ServerListTab.All && (serverInfo.LobbyID != 0 || serverInfo.Endpoint != null)) || - (selectedTab == ServerListTab.Recent && serverInfo.Recent) || - (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); - } - - foreach (GUITickBox tickBox in playStyleTickBoxes.Values) - { - var playStyle = (PlayStyle)tickBox.UserData; - if (!tickBox.Selected && (serverInfo.PlayStyle == playStyle || !serverInfo.PlayStyle.HasValue)) - { - child.Visible = false; - break; - } - } - - foreach (GUITickBox tickBox in gameModeTickBoxes.Values) - { - var gameMode = (Identifier)tickBox.UserData; - if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode == gameMode) - { - child.Visible = false; - break; - } - } - } - - if (serverList.Content.Children.All(c => !c.Visible)) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverList.Content.RectTransform), - TextManager.Get("NoMatchingServers")) - { - UserData = "noresults" - }; - } - - serverList.UpdateScrollBarSize(); - } - - private readonly Queue pendingQueries = new Queue(); - int activeQueries = 0; - private void QueueInfoQuery(ServerInfo info) - { - pendingQueries.Enqueue(info); - } - - private void OnQueryDone(ServerInfo info) - { - activeQueries--; - } - - public void UpdateInfoQueries() - { - while (activeQueries < 25 && pendingQueries.Count > 0) - { - activeQueries++; - var info = pendingQueries.Dequeue(); - info.QueryLiveInfo(UpdateServerInfo, OnQueryDone); - } - } - - private void ShowDirectJoinPrompt() - { - var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", - new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); - msgBox.Content.ChildAnchor = Anchor.TopCenter; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter) - { - IgnoreLayoutGroups = false, - Stretch = true, - RelativeSpacing = 0.05f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); - var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); - - content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); - content.RectTransform.IsFixedSize = true; - msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f)); - - var okButton = msgBox.Buttons[0]; - okButton.Enabled = false; - okButton.OnClicked = (btn, userdata) => - { - if (!(Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))) { return false; } - JoinServer(endpoint, ""); - msgBox.Close(); - return false; - }; - - var favoriteButton = msgBox.Buttons[1]; - favoriteButton.Enabled = false; - favoriteButton.OnClicked = (button, userdata) => - { - if (!(Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))) { return false; } - - ServerInfo serverInfo = new ServerInfo() - { - ServerName = "Server", - Endpoint = endpoint, - GameVersion = GameMain.Version.ToString(), - PlayStyle = null - }; - - var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - info.MatchesByEndpoint(serverInfo)); - - if (serverFrame != null) - { - serverInfo = serverFrame.UserData as ServerInfo; - } - else - { - AddToServerList(serverInfo); - } - - AddToFavoriteServers(serverInfo); - - SelectedTab = ServerListTab.Favorites; - FilterServers(); - - QueueInfoQuery(serverInfo); - - msgBox.Close(); - return false; - }; - - var cancelButton = msgBox.Buttons[2]; - cancelButton.OnClicked = msgBox.Close; - - endpointBox.OnTextChanged += (textBox, text) => - { - okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); - return true; - }; - } - - private bool JoinFriend(GUIButton button, object userdata) - { - if (!(userdata is FriendInfo { InServer: true } info)) { return false; } - - GameMain.Instance.ConnectCommand = info.ConnectCommand; - return false; - } - - private bool OpenFriendPopup(GUIButton button, object userdata) - { - if (!(userdata is FriendInfo { InServer: true } info)) { return false; } - - if (info.InServer - && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } - && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) - { - const int framePadding = 5; - - friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); - - var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), nameAndEndpoint.ServerName ?? "[Unnamed]"); - serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) - { - UserData = info - }; - joinButton.OnClicked = JoinFriend; - joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); - int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; - int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); - - // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. - Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); - - // Add padding to the server name to match the padding on the button text. - serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); - - // Get the dimensions of the text we want to show, plus the extra padding we added. - Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); - - // Now determine how large the parent frame has to be to exactly fit our two controls. - Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); - - var popupPos = PlayerInput.MousePosition.ToPoint(); - if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) - { - // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. - popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; - } - - // Apply the size and position changes. - friendPopup.RectTransform.NonScaledSize = frameDims; - friendPopup.RectTransform.RelativeOffset = Vector2.Zero; - friendPopup.RectTransform.AbsoluteOffset = popupPos; - - joinButton.RectTransform.NonScaledSize = finalButtonSize; - - friendPopup.RectTransform.RecalculateChildren(true); - friendPopup.RectTransform.SetPosition(Anchor.TopLeft); - } - - return false; - } - - private enum AvatarSize - { - Small, - Medium, - Large - } - - private void UpdateFriendsList() - { - if (!SteamManager.IsInitialized) { return; } - - if (friendsListUpdateTime > Timing.TotalTime) { return; } - friendsListUpdateTime = Timing.TotalTime + 5.0; - - float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; - - if (friendsDropdown == null) - { - friendsDropdown = new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) - { - OutlineColor = Color.Black, - Visible = false - }; - } - friendsDropdown.ClearChildren(); - - AvatarSize avatarSize = AvatarSize.Large; - if (friendsButtonHolder.RectTransform.Rect.Height <= 24) - { - avatarSize = AvatarSize.Small; - } - else if (friendsButtonHolder.RectTransform.Rect.Height <= 48) - { - avatarSize = AvatarSize.Medium; - } - - List friends = Steamworks.SteamFriends.GetFriends().ToList(); - - for (int i = friendsList.Count - 1; i >= 0; i--) - { - var friend = friendsList[i]; - if (!friends.Any(g => g.Id == friend.SteamId && g.IsOnline)) - { - friend.Sprite?.Remove(); - friendsList.RemoveAt(i); - } - } - - foreach (var friend in friends) - { - if (!friend.IsOnline) { continue; } - - FriendInfo info = friendsList.Find(f => f.SteamId == friend.Id); - if (info == null) - { - info = new FriendInfo() - { - SteamId = friend.Id - }; - friendsList.Insert(0, info); - } - - if (info.Sprite == null) - { - Func> avatarFunc = null; - switch (avatarSize) - { - case AvatarSize.Small: - avatarFunc = Steamworks.SteamFriends.GetSmallAvatarAsync; - break; - case AvatarSize.Medium: - avatarFunc = Steamworks.SteamFriends.GetMediumAvatarAsync; - break; - case AvatarSize.Large: - avatarFunc = Steamworks.SteamFriends.GetLargeAvatarAsync; - break; - } - TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(friend.Id), (task) => - { - if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } - if (!img.HasValue) { return; } - - var avatarImage = img.Value; - - const int desaturatedWeight = 180; - - byte[] avatarData = (byte[])avatarImage.Data.Clone(); - for (int i = 0; i < avatarData.Length; i += 4) - { - int luma = (avatarData[i + 0] * 299 + avatarData[i + 1] * 587 + avatarData[i + 2] * 114) / 1000; - luma = (int)(luma * 0.7f + ((luma / 100.0f) * (luma / 255.0f) * 255.0f * 0.3f)); - int chn0 = ((avatarData[i + 0] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn1 = ((avatarData[i + 1] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn2 = ((avatarData[i + 2] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); - int chn3 = 255; - - chn0 = chn0 * chn3 / 255; - chn1 = chn1 * chn3 / 255; - chn2 = chn2 * chn3 / 255; - - avatarData[i + 0] = chn0 > 255 ? (byte)255 : (byte)chn0; - avatarData[i + 1] = chn1 > 255 ? (byte)255 : (byte)chn1; - avatarData[i + 2] = chn2 > 255 ? (byte)255 : (byte)chn2; - avatarData[i + 3] = chn3 > 255 ? (byte)255 : (byte)chn3; - } - CrossThread.RequestExecutionOnMainThread(() => - { - //TODO: create an avatar atlas? - var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); - avatarTexture.SetData(avatarData); - info.Sprite = new Sprite(avatarTexture, null, null); - }); - }); - } - - info.Name = friend.Name; - - info.ConnectCommand = Option.None(); - - info.PlayingThisGame = friend.IsPlayingThisGame; - info.PlayingAnotherGame = friend.GameInfo.HasValue; - - if (friend.IsPlayingThisGame) - { - info.StatusText = friend.GetRichPresence("status") ?? ""; - string connectCommand = friend.GetRichPresence("connect") ?? ""; - - try - { - info.ConnectCommand = ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand)); - } - catch (IndexOutOfRangeException e) - { -#if DEBUG - DebugConsole.ThrowError($"Failed to parse a Steam friend's connect command ({connectCommand})", e); -#else - DebugConsole.Log($"Failed to parse a Steam friend's connect command ({connectCommand})\n" + e.StackTrace.CleanupStackTrace()); -#endif - info.ConnectCommand = Option.None(); - } - } - else - { - info.StatusText = TextManager.Get(info.PlayingAnotherGame ? "FriendPlayingAnotherGame" : "FriendNotPlaying"); - } - } - - friendsList.Sort((a, b) => - { - if (a.InServer && !b.InServer) { return -1; } - if (b.InServer && !a.InServer) { return 1; } - if (a.PlayingThisGame && !b.PlayingThisGame) { return -1; } - if (b.PlayingThisGame && !a.PlayingThisGame) { return 1; } - return 0; - }); - - friendsButtonHolder.ClearChildren(); - - if (friendsList.Count > 0) - { - friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") - { - OnClicked = (button, udt) => - { - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - friendsDropdown.Visible = !friendsDropdown.Visible; - return false; - } - }; - } - else - { - friendsDropdownButton = null; - friendsDropdown.Visible = false; - } - - int buttonCount = 0; - - for (int i = 0; i < friendsList.Count; i++) - { - var friend = friendsList[i]; - buttonCount++; - - if (buttonCount <= 5) - { - string style = "GUIButtonFriendNotPlaying"; - if (friend.InServer) - { - style = "GUIButtonFriendPlaying"; - } - else - { - style = friend.PlayingThisGame ? "GUIButtonFriendPlaying" : "GUIButtonFriendNotPlaying"; - } - - var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) - { - UserData = friend, - OnClicked = OpenFriendPopup - }; - guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; - - if (friend.Sprite != null) - { - static Color BrightenColor(Color color) - { - Vector3 hls = ToolBox.RgbToHLS(color); - hls.Y = hls.Y * 0.3f + 0.7f; - hls.Z = hls.Z * 0.6f + 0.4f; - - return ToolBox.HLSToRGB(hls); - } - - var imgColor = BrightenColor(guiButton.Color); - var imgHoverColor = BrightenColor(guiButton.HoverColor); - var imgSelectColor = BrightenColor(guiButton.SelectedColor); - var imgPressColor = BrightenColor(guiButton.PressedColor); - var guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) - { - Color = imgColor, - HoverColor = imgHoverColor, - SelectedColor = imgSelectColor, - PressedColor = imgPressColor, - CanBeFocused = false - }; - guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) - { - Color = Color.White * 0.8f, - HoverColor = Color.White * 0.8f, - SelectedColor = Color.White * 0.8f, - PressedColor = Color.White * 0.8f, - BlendState = BlendState.Additive, - CanBeFocused = false - }; - } - } - - var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); - var guiImage2TheSequel = new GUIImage(new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) } , friend.Sprite, null, true); - - var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) - { - Font = GUIStyle.SmallFont - }; - if (friend.PlayingThisGame) { textBlock.TextColor = GUIStyle.Green; } - if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } - - if (friend.InServer) - { - var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") - { - UserData = friend - }; - joinButton.OnClicked = JoinFriend; - } - } - - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - - friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; - } - - private void RefreshServers() - { - if (waitingForRefresh) { return; } - - steamPingInfoReady = false; - - CoroutineManager.StopCoroutines("EstimateLobbyPing"); - - if (SteamManager.IsInitialized) - { - TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => - { - steamPingInfoReady = true; - }); - } - - friendsListUpdateTime = Timing.TotalTime - 1.0; - UpdateFriendsList(); - - serverList.ClearChildren(); - serverPreview.Content.ClearChildren(); - joinButton.Enabled = false; - selectedServer = null; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), - TextManager.Get("RefreshingServerList"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - - CoroutineManager.StartCoroutine(WaitForRefresh()); - } - - private IEnumerable WaitForRefresh() - { - waitingForRefresh = true; - if (refreshDisableTimer > DateTime.Now) - { - yield return new WaitForSeconds((float)(refreshDisableTimer - DateTime.Now).TotalSeconds); - } - - recentServers.Concat(favoriteServers).ForEach(si => si.OwnerVerified = false); - if (GameSettings.CurrentConfig.UseSteamMatchmaking) - { - serverList.ClearChildren(); - if (!SteamManager.GetServers(AddToServerList, ServerQueryFinished)) - { - serverList.ClearChildren(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), - TextManager.Get("ServerListNoSteamConnection"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - scanServersButton.Enabled = false; - } - else - { - List knownServers = recentServers.Concat(favoriteServers).ToList(); - foreach (ServerInfo info in knownServers) - { - AddToServerList(info); - QueueInfoQuery(info); - } - scanServersButton.Enabled = true; - } - } - - refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; - - yield return CoroutineStatus.Success; - } - - private 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 = FindFrameMatchingServerInfo(serverInfo); - - if (serverFrame == null) - { - serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, - style: "ListBoxElement") - { - UserData = serverInfo - }; - new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - //RelativeSpacing = 0.02f - }; - } - else - { - int index = recentServers.IndexOf(serverFrame.UserData as ServerInfo); - if (index >= 0) - { - recentServers[index] = serverInfo; - serverInfo.Recent = true; - } - index = favoriteServers.IndexOf(serverFrame.UserData as ServerInfo); - if (index >= 0) - { - favoriteServers[index] = serverInfo; - serverInfo.Favorite = true; - } - } - serverFrame.UserData = serverInfo; - - if (serverInfo.OwnerVerified) - { - var childrenToRemove = serverList.Content.FindChildren(c => - c.UserData is ServerInfo info - && !ReferenceEquals(info, serverInfo) - && serverInfo.Endpoint == info.Endpoint).ToList(); - foreach (var child in childrenToRemove) - { - serverList.Content.RemoveChild(child); - } - } - - UpdateServerInfo(serverInfo); - - SortList(sortedBy, toggle: false); - FilterServers(); - } - - private void UpdateServerInfo(ServerInfo serverInfo) - { - var serverFrame = FindFrameMatchingServerInfo(serverInfo); - if (serverFrame == null) return; - - var serverContent = serverFrame.Children.First() as GUILayoutGroup; - serverContent.ClearChildren(); - - var compatibleBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[0], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") - { - CanBeFocused = false, - Selected = - (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true), - UserData = "compatible" - }; - - var passwordBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[1], 0.5f), serverContent.RectTransform, Anchor.Center), label: "", style: "GUIServerListPasswordTickBox") - { - ToolTip = TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword"), - Selected = serverInfo.HasPassword, - CanBeFocused = false, - UserData = "password" - }; - - var serverName = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[2] * 1.1f, 1.0f), serverContent.RectTransform), -#if DEBUG - (serverInfo.Endpoint is SteamP2PEndpoint ? "[STEAMP2P] " : "[LIDGREN] ") + -#endif - serverInfo.ServerName, - style: "GUIServerListTextBox"); - serverName.UserData = serverName.Text; - serverName.RectTransform.SizeChanged += () => - { - serverName.Text = ToolBox.LimitString(serverName.Text, serverName.Font, serverName.Rect.Width); - }; - - if (serverInfo.ContentPackageNames.Any()) - { - if (serverInfo.ContentPackageNames.Any(p => !GameMain.VanillaContent.NameMatches(p))) - { - serverName.TextColor = GUIStyle.ModdedServerColor; - } - } - - new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[3], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") - { - ToolTip = TextManager.Get((serverInfo.GameStarted) ? "ServerListRoundStarted" : "ServerListRoundNotStarted"), - Selected = serverInfo.GameStarted, - CanBeFocused = false - }; - - var serverPlayers = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[4], 1.0f), serverContent.RectTransform), - serverInfo.PlayerCount + "/" + serverInfo.MaxPlayers, style: "GUIServerListTextBox", textAlignment: Alignment.Right) - { - ToolTip = TextManager.Get("ServerListPlayers") - }; - - var serverPingText = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[5], 1.0f), serverContent.RectTransform), "?", - style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) - { - ToolTip = TextManager.Get("ServerListPing") - }; - - if (serverInfo.PingChecked) - { - serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; - serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); - } - else if (serverInfo.Endpoint is LidgrenEndpoint lidgrenEndpoint) - { - try - { - GetServerPing(serverInfo, serverPingText); - } - catch (NullReferenceException ex) - { - DebugConsole.ThrowError("Ping is null", ex); - } - } - else if (serverInfo.PingLocation != null) - { - CoroutineManager.StartCoroutine(EstimateLobbyPing(serverInfo, serverPingText), "EstimateLobbyPing"); - } - - if (serverInfo.LobbyID == 0) - { - LocalizedString toolTip = TextManager.Get("ServerOffline"); - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - serverName.TextColor *= 0.8f; - serverPlayers.TextColor *= 0.8f; - } - else if (GameSettings.CurrentConfig.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) - { - LocalizedString toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); - compatibleBox.Selected = false; - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - serverName.TextColor *= 0.8f; - serverPlayers.TextColor *= 0.8f; - } - else if (string.IsNullOrEmpty(serverInfo.GameVersion) || !serverInfo.ContentPackageHashes.Any()) - { - compatibleBox.Selected = false; - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) - { - ToolTip = TextManager.Get(string.IsNullOrEmpty(serverInfo.GameVersion) ? - "ServerListUnknownVersion" : - "ServerListUnknownContentPackage") - }; - } - else if (!compatibleBox.Selected) - { - LocalizedString toolTip = ""; - if (serverInfo.GameVersion != GameMain.Version.ToString()) - { - toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); - } - - int maxIncompatibleToList = 10; - List incompatibleModNames = new List(); - for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) - { - bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); - if (listAsIncompatible) - { - incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", - ("[name]", serverInfo.ContentPackageNames[i]), - ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i])))); - - } - } - if (incompatibleModNames.Any()) - { - toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); - if (incompatibleModNames.Count > maxIncompatibleToList) - { - toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); - } - } - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - - serverName.TextColor *= 0.5f; - serverPlayers.TextColor *= 0.5f; - } - else - { - LocalizedString toolTip = ""; - for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) - { - if (ContentPackageManager.EnabledPackages.All.None(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) - { - if (toolTip != "") { toolTip += "\n"; } - toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); - break; - } - } - serverContent.Children.ForEach(c => c.ToolTip = toolTip); - } - - serverContent.Recalculate(); - - if (serverInfo.Favorite) - { - AddToFavoriteServers(serverInfo); - } - - SortList(sortedBy, toggle: false); - FilterServers(); - } - - private IEnumerable EstimateLobbyPing(ServerInfo serverInfo, GUITextBlock serverPingText) - { - while (!steamPingInfoReady) - { - yield return CoroutineStatus.Running; - } - - Steamworks.Data.NetPingLocation pingLocation = serverInfo.PingLocation.Value; - serverInfo.Ping = Steamworks.SteamNetworkingUtils.LocalPingLocation?.EstimatePingTo(pingLocation) ?? -1; - serverInfo.PingChecked = true; - serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); - serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; - - yield return CoroutineStatus.Success; - } - - private void ServerQueryFinished() - { - if (!serverList.Content.Children.Any()) - { - new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), - TextManager.Get("NoServers"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else if (serverList.Content.Children.All(c => !c.Visible)) - { - new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), - TextManager.Get("NoMatchingServers"), textAlignment: Alignment.Center) - { - CanBeFocused = false, - UserData = "noresults" - }; - } - waitingForRefresh = false; - } - - private void MasterServerCallBack(IRestResponse response) - { - masterServerResponse = response; - masterServerResponded = true; - } - - private bool JoinServer(Endpoint endpoint, string serverName) - { - if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) - { - ClientNameBox.Flash(); - ClientNameBox.Select(); - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - return false; - } - - MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; - GameSettings.SaveCurrentConfig(); - - CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName), "ConnectToServer"); - - return true; - } - - private IEnumerable ConnectToServer(Endpoint endpoint, string serverName) - { -#if !DEBUG - try - { -#endif - GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), endpoint, serverName, Option.None()); -#if !DEBUG - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to start the client", e); - } -#endif - - yield return CoroutineStatus.Success; - } - - 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(address)) { return; } - activePings.Add(address, activePings.Any() ? activePings.Values.Max()+1 : 0); - } - - serverInfo.PingChecked = false; - serverInfo.Ping = -1; - - TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), - new Tuple(serverInfo, serverPingText), - (rtt, obj) => - { - 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(address); - } - }); - } - - private Color GetPingTextColor(int ping) - { - if (ping < 0) { return Color.DarkRed; } - return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); - } - - public async Task PingServerAsync(IPAddress ipAddress, int timeOut) - { - await Task.Yield(); - bool shouldGo = false; - while (!shouldGo) - { - lock (activePings) - { - shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; - } - await Task.Delay(25); - } - - long rtt = -1; - if (ipAddress != null) - { - //don't attempt to ping if the address is IPv6 and it's not supported - if (ipAddress.AddressFamily != AddressFamily.InterNetworkV6 || Socket.OSSupportsIPv6) - { - Ping ping = new Ping(); - byte[] buffer = new byte[32]; - try - { - PingReply pingReply = ping.Send(ipAddress, timeOut, buffer, new PingOptions(128, true)); - - if (pingReply != null) - { - switch (pingReply.Status) - { - case IPStatus.Success: - rtt = pingReply.RoundtripTime; - break; - default: - rtt = -1; - break; - } - } - } - catch (Exception ex) - { - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); -#if DEBUG - DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); -#endif - } - } - } - - return (int)rtt; - } - - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) - { - graphics.Clear(Color.CornflowerBlue); - - GameMain.TitleScreen.DrawLoadingText = false; - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - - GUI.Draw(Cam, spriteBatch); - - spriteBatch.End(); - } - - public override void AddToGUIUpdateList() - { - menu.AddToGUIUpdateList(); - friendPopup?.AddToGUIUpdateList(); - friendsDropdown?.AddToGUIUpdateList(); - } - - public void StoreServerFilters() - { - foreach (KeyValuePair filterBox in filterTickBoxes) - { - ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); - } - foreach (KeyValuePair ternaryFilter in ternaryFilters) - { - ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); - } - } - - public void LoadServerFilters() - { - XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); - ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); - foreach (KeyValuePair filterBox in filterTickBoxes) - { - filterBox.Value.Selected = - ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); - } - foreach (KeyValuePair ternaryFilter in ternaryFilters) - { - TernaryOption ternaryOption = - ServerListFilters.Instance.GetAttributeEnum( - ternaryFilter.Key, - (TernaryOption)ternaryFilter.Value.SelectedData); - - var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); - ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); - } - } - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs new file mode 100644 index 000000000..f8cece982 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/PanelAnimator.cs @@ -0,0 +1,112 @@ +using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + public class PanelAnimator + { + private readonly GUIScissorComponent container; + + private readonly GUIFrame leftFrame; + private readonly GUIComponent middleFrame; + private readonly GUIFrame rightFrame; + + private readonly GUIButton leftButton; + private readonly GUIButton rightButton; + + private float leftAnimState = 1.0f; + private float rightAnimState = 0.0f; + + public bool LeftEnabled + { + get => leftButton.Enabled; + set => leftButton.Enabled = value; + } + public bool RightEnabled + { + get => rightButton.Enabled; + set => rightButton.Enabled = value; + } + + public bool LeftVisible = true; + public bool RightVisible = false; + + public PanelAnimator(RectTransform rectTransform, GUIFrame leftFrame, GUIComponent middleFrame, GUIFrame rightFrame) + { + container = new GUIScissorComponent(rectTransform); + + this.leftFrame = leftFrame; + this.middleFrame = middleFrame; + this.rightFrame = rightFrame; + + void own(GUIComponent component) + { + component.RectTransform.Parent = container.Content.RectTransform; + component.RectTransform.Anchor = Anchor.TopLeft; + component.RectTransform.Pivot = Pivot.TopLeft; + + component.GetAllChildren().ForEach(dd => dd.RefreshListBoxParent()); + } + + GUIButton makeButton(Action action) + => new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), container.Content.RectTransform) + { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, + style: "UIToggleButton") + { + OnClicked = (_, __) => + { + action(); + return false; + } + }; + + own(leftFrame); + this.leftButton = makeButton(() => LeftVisible = !LeftVisible); + + own(middleFrame); + + this.rightButton = makeButton(() => RightVisible = !RightVisible); + own(rightFrame); + } + + public void Update() + { + if (!LeftEnabled) { LeftVisible = false; } + if (!RightEnabled) { RightVisible = false; } + + static void updateState(ref float state, bool visible) + => state = MathHelper.Lerp(state, visible ? 0.0f : 1.0f, 0.5f); + updateState(ref leftAnimState, LeftVisible); + updateState(ref rightAnimState, RightVisible); + + static int width(GUIComponent c) + => c.RectTransform.NonScaledSize.X; + + int height = container.RectTransform.NonScaledSize.Y; + int buttonY = height/2 - leftButton.RectTransform.NonScaledSize.Y/2; + + leftFrame.RectTransform.AbsoluteOffset = new Point((int)(-width(leftFrame) * leftAnimState), 0); + leftButton.RectTransform.AbsoluteOffset = leftFrame.RectTransform.AbsoluteOffset + + new Point(width(leftFrame), buttonY); + leftButton.Children.ForEach(c => c.SpriteEffects = LeftVisible + ? SpriteEffects.FlipHorizontally + : SpriteEffects.None); + + rightFrame.RectTransform.AbsoluteOffset = new Point((int)(width(container) + width(rightFrame) * (rightAnimState-1f)), 0); + rightButton.RectTransform.AbsoluteOffset = rightFrame.RectTransform.AbsoluteOffset + + new Point(-width(rightButton), buttonY); + rightButton.Children.ForEach(c => c.SpriteEffects = RightVisible + ? SpriteEffects.None + : SpriteEffects.FlipHorizontally); + + middleFrame.RectTransform.AbsoluteOffset = new Point( + leftButton.RectTransform.AbsoluteOffset.X + width(leftButton), + 0); + middleFrame.RectTransform.NonScaledSize = new Point( + rightButton.RectTransform.AbsoluteOffset.X - middleFrame.RectTransform.AbsoluteOffset.X, + height); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs new file mode 100644 index 000000000..8e54407a4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -0,0 +1,1676 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class ServerListScreen : Screen + { + private enum MsgUserData + { + RefreshingServerList, + NoServers, + NoMatchingServers + } + + //how often the client is allowed to refresh servers + private static readonly TimeSpan AllowedRefreshInterval = TimeSpan.FromSeconds(3); + + private DateTime lastRefreshTime = DateTime.Now; + + private GUIFrame menu; + + private GUIListBox serverList; + private PanelAnimator panelAnimator; + private GUIFrame serverPreviewContainer; + private GUIListBox serverPreview; + + private GUIButton joinButton; + private Option selectedServer; + + private GUIButton scanServersButton; + + private enum TernaryOption + { + Any, + Enabled, + Disabled + } + + //friends list + public sealed class FriendInfo + { + public string Name; + + public readonly AccountId Id; + + public enum Status + { + Offline, + NotPlaying, + PlayingAnotherGame, + PlayingBarotrauma + } + + public readonly Status CurrentStatus; + + public string ServerName; + + public Option ConnectCommand; + public Option Avatar; + + public bool IsInServer + => CurrentStatus == Status.PlayingBarotrauma && ConnectCommand.IsSome(); + + public bool IsPlayingBarotrauma + => CurrentStatus == Status.PlayingBarotrauma; + + public bool PlayingAnotherGame + => CurrentStatus == Status.PlayingAnotherGame; + + public bool IsOnline + => CurrentStatus != Status.Offline; + + public LocalizedString StatusText + => CurrentStatus switch + { + Status.Offline => "", + _ when ConnectCommand.IsSome() + => TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName), + _ => TextManager.Get($"Friend{CurrentStatus}") + }; + + public FriendInfo(string name, AccountId id, Status status) + { + Name = name; + Id = id; + CurrentStatus = status; + ConnectCommand = Option.None(); + Avatar = Option.None(); + } + } + + private GUILayoutGroup friendsButtonHolder; + + private GUIButton friendsDropdownButton; + private GUIListBox friendsDropdown; + + private readonly FriendProvider friendProvider = new SteamFriendProvider(); + + private List friendsList; + private GUIFrame friendPopup; + private double friendsListUpdateTime; + + public enum TabEnum + { + All, + Favorites, + Recent + } + + public struct Tab + { + public readonly string Storage; + public readonly GUIButton Button; + + private readonly List servers; + public IReadOnlyList Servers => servers; + + public Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage) + { + Storage = storage; + servers = new List(); + Button = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabber.RectTransform), + TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton") + { + OnClicked = (_,__) => + { + serverListScreen.selectedTab = tabEnum; + return false; + } + }; + + Reload(); + } + + public void Reload() + { + if (Storage.IsNullOrEmpty()) { return; } + servers.Clear(); + XDocument doc = XMLExtensions.TryLoadXml(Storage, out _); + if (doc?.Root is null) { return; } + servers.AddRange(doc.Root.Elements().Select(ServerInfo.FromXElement).NotNone().Distinct()); + } + + public bool Contains(ServerInfo info) => servers.Contains(info); + public bool Remove(ServerInfo info) => servers.Remove(info); + public void AddOrUpdate(ServerInfo info) + { + servers.Remove(info); servers.Add(info); + } + + public void Clear() => servers.Clear(); + + public void Save() + { + XDocument doc = new XDocument(); + XElement rootElement = new XElement("servers"); + doc.Add(rootElement); + + foreach (ServerInfo info in servers) + { + rootElement.Add(info.ToXElement()); + } + + doc.SaveSafe(Storage); + } + } + + private readonly Dictionary tabs = new Dictionary(); + + private TabEnum _selectedTabBackingField; + private TabEnum selectedTab + { + get => _selectedTabBackingField; + set + { + _selectedTabBackingField = value; + tabs.ForEach(kvp => kvp.Value.Button.Selected = (value == kvp.Key)); + if (Screen.Selected == this) { RefreshServers(); } + } + } + + private readonly ServerProvider serverProvider + = new CompositeServerProvider(new SteamDedicatedServerProvider(), new SteamP2PServerProvider()); + + public GUITextBox ClientNameBox { get; private set; } + + enum ColumnLabel + { + ServerListCompatible, + ServerListHasPassword, + ServerListName, + ServerListRoundStarted, + ServerListPlayers, + ServerListPing + } + private struct Column + { + public float RelativeWidth; + public ColumnLabel Label; + + public static implicit operator Column((float W, ColumnLabel L) pair) => + new Column { RelativeWidth = pair.W, Label = pair.L }; + + public static Column[] Normalize(params Column[] columns) + { + var totalWidth = columns.Select(c => c.RelativeWidth).Aggregate((a, b) => a + b); + for (int i = 0; i < columns.Length; i++) + { + columns[i].RelativeWidth /= totalWidth; + } + return columns; + } + } + + private static readonly ImmutableDictionary columns = + Column.Normalize( + (0.1f, ColumnLabel.ServerListCompatible), + (0.1f, ColumnLabel.ServerListHasPassword), + (0.7f, ColumnLabel.ServerListName), + (0.12f, ColumnLabel.ServerListRoundStarted), + (0.08f, ColumnLabel.ServerListPlayers), + (0.08f, ColumnLabel.ServerListPing) + ).Select(c => (c.Label, c)).ToImmutableDictionary(); + + private GUILayoutGroup labelHolder; + private readonly List labelTexts = new List(); + + //filters + private GUITextBox searchBox; + private GUITickBox filterSameVersion; + private GUITickBox filterPassword; + private GUITickBox filterFull; + private GUITickBox filterEmpty; + private Dictionary ternaryFilters; + private Dictionary filterTickBoxes; + private Dictionary playStyleTickBoxes; + private Dictionary gameModeTickBoxes; + private GUITickBox filterOffensive; + + //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. + private TernaryOption filterFriendlyFireValue = TernaryOption.Any; + private TernaryOption filterKarmaValue = TernaryOption.Any; + private TernaryOption filterTraitorValue = TernaryOption.Any; + private TernaryOption filterVoipValue = TernaryOption.Any; + private TernaryOption filterModdedValue = TernaryOption.Any; + + private ColumnLabel sortedBy; + + private const float sidebarWidth = 0.2f; + public ServerListScreen() + { + selectedServer = Option.None(); + GameMain.Instance.ResolutionChanged += CreateUI; + CreateUI(); + } + + private string GetDefaultUserName() + { + return friendProvider.GetUserName(); + } + + private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) + { + var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) + { + Stretch = true + }; + + var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) + { + IsFixedSize = true, + }, null) + { + HoverColor = Color.Gray, + SelectedColor = Color.DarkGray, + CanBeFocused = false + }; + if (box.RectTransform.MinSize.Y > 0) + { + box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); + box.RectTransform.Resize(box.RectTransform.MinSize); + } + Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); + + var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) + { + UserData = TextManager.Get($"servertag.{tag}.label") + }; + GUIStyle.Apply(filterLabel, "GUITextBlock", null); + + var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); + dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); + dropDown.AddItem(TextManager.Get($"servertag.{tag}.true"), TernaryOption.Enabled, TextManager.Get( + $"servertagdescription.{tag}.true")); + dropDown.AddItem(TextManager.Get($"servertag.{tag}.false"), TernaryOption.Disabled, TextManager.Get( + $"servertagdescription.{tag}.false")); + dropDown.SelectItem(TernaryOption.Any); + dropDown.OnSelected = (_, data) => { + valueSetter((TernaryOption)data); + FilterServers(); + StoreServerFilters(); + return true; + }; + + ternaryFilters.Add(tag, dropDown); + } + + private void CreateUI() + { + menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + //------------------------------------------------------------------------------------- + //Top row + //------------------------------------------------------------------------------------- + + var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; + + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) + { + Padding = Vector4.Zero, + ForceUpperCase = ForceUpperCase.Yes, + AutoScaleHorizontal = true + }; + + var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; + + var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); + ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") + { + Text = MultiplayerPreferences.Instance.PlayerName, + MaxTextLength = Client.MaxNameLength, + OverflowClip = true + }; + + if (string.IsNullOrEmpty(ClientNameBox.Text)) + { + ClientNameBox.Text = GetDefaultUserName(); + } + ClientNameBox.OnTextChanged += (textbox, text) => + { + MultiplayerPreferences.Instance.PlayerName = text; + return true; + }; + + var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); + + tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, ""); + tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml"); + tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml"); + + var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") + { + IgnoreLayoutGroups = true + }; + + friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; + friendsList = new List(); + + //------------------------------------------------------------------------------------- + // Bottom row + //------------------------------------------------------------------------------------- + + var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y), + paddedFrame.RectTransform, Anchor.CenterRight)) + { + Stretch = true + }; + + var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + OutlineColor = Color.Black + }; + + GUILayoutGroup serverListContainer = null; + GUIFrame filtersHolder = null; + + // filters ------------------------------------------- + + filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + { + Color = new Color(12, 14, 15, 255) * 0.5f, + OutlineColor = Color.Black + }; + + float elementHeight = 0.05f; + var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) + { + Padding = Vector4.Zero, + AutoScaleHorizontal = true, + CanBeFocused = false + }; + + var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true }; + + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); + searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; + + var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft)) + { + ScrollBarVisible = true, + Spacing = (int)(5 * GUI.Scale) + }; + + ternaryFilters = new Dictionary(); + filterTickBoxes = new Dictionary(); + + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) + { + text ??= TextManager.Get(key); + var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + { + UserData = text, + Selected = defaultState, + ToolTip = addTooltip ? text : null, + OnSelected = (tickBox) => + { + FilterServers(); + StoreServerFilters(); + return true; + } + }; + filterTickBoxes.Add(key, tickBox); + return tickBox; + } + + filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); + filterPassword = addTickBox("FilterPassword".ToIdentifier()); + filterFull = addTickBox("FilterFullServers".ToIdentifier()); + filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); + filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + + // Filter Tags + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); + + // Play Style Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + playStyleTickBoxes = new Dictionary(); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); + selectionTick.UserData = playStyle; + playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); + } + + // Game mode Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + + gameModeTickBoxes = new Dictionary(); + foreach (GameModePreset mode in GameModePreset.List) + { + if (mode.IsSinglePlayer) { continue; } + + var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); + selectionTick.UserData = mode.Identifier; + gameModeTickBoxes.Add(mode.Identifier, selectionTick); + } + + filters.Content.RectTransform.SizeChanged += () => + { + filters.Content.RectTransform.RecalculateChildren(true, true); + filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); + gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + GUITextBlock.AutoScaleAndNormalize( + filterTickBoxes.Values.Select(tb => tb.TextBlock) + .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), + defaultScale: 1.0f); + if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) + { + filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); + filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f))); + } + }; + + // server list --------------------------------------------------------------------- + + serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; + + labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, + isHorizontal: true, childAnchor: Anchor.BottomLeft) + { + Stretch = false + }; + + foreach (var column in columns.Values) + { + var label = TextManager.Get(column.Label.ToString()); + var btn = new GUIButton(new RectTransform(new Vector2(column.RelativeWidth, 1.0f), labelHolder.RectTransform), + text: label, textAlignment: Alignment.Center, style: "GUIButtonSmall") + { + ToolTip = label, + ForceUpperCase = ForceUpperCase.Yes, + UserData = column.Label, + OnClicked = SortList + }; + btn.Color *= 0.5f; + labelTexts.Add(btn.TextBlock); + + GUIImage arrowImg(object userData, SpriteEffects sprEffects) + => new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) + { + CanBeFocused = false, + UserData = userData, + SpriteEffects = sprEffects, + Visible = false + }; + + arrowImg("arrowup", SpriteEffects.None); + arrowImg("arrowdown", SpriteEffects.FlipVertically); + } + + serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) + { + PlaySoundOnSelect = true, + ScrollBarVisible = true, + OnSelected = (btn, obj) => + { + if (!(obj is ServerInfo serverInfo)) { return false; } + + joinButton.Enabled = true; + selectedServer = Option.Some(serverInfo); + if (!serverPreviewContainer.Visible) + { + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); + serverPreviewContainer.Visible = true; + serverPreviewContainer.IgnoreLayoutGroups = false; + } + serverInfo.CreatePreviewWindow(serverPreview.Content); + serverPreview.ForceLayoutRecalculation(); + panelAnimator.RightEnabled = true; + panelAnimator.RightVisible = true; + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + return true; + } + }; + + //server preview panel -------------------------------------------------- + serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + { + Color = new Color(12, 14, 15, 255) * 0.5f, + OutlineColor = Color.Black, + IgnoreLayoutGroups = true + }; + serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) + { + Padding = Vector4.One * 10 * GUI.Scale, + HoverCursor = CursorState.Default, + OnSelected = (component, o) => false + }; + + panelAnimator = new PanelAnimator(new RectTransform(Vector2.One, serverListHolder.RectTransform), + filtersHolder, + serverListContainer, + serverPreviewContainer); + panelAnimator.RightEnabled = false; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Back")) + { + OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu + }; + + scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("ServerListRefresh")) + { + OnClicked = (btn, userdata) => { RefreshServers(); return true; } + }; + + var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("serverlistdirectjoin")) + { + OnClicked = (btn, userdata) => + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + return false; + } + ShowDirectJoinPrompt(); + return true; + } + }; + + joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("ServerListJoin")) + { + OnClicked = (btn, userdata) => + { + if (selectedServer.TryUnwrap(out var serverInfo)) + { + JoinServer(serverInfo.Endpoint, serverInfo.ServerName); + } + return true; + }, + Enabled = false + }; + + buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f)); + + //-------------------------------------------------------- + + bottomRow.Recalculate(); + serverListHolder.Recalculate(); + serverListContainer.Recalculate(); + labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); + labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); + labelHolder.Recalculate(); + + serverList.Content.RectTransform.SizeChanged += () => + { + labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); + labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); + labelHolder.Recalculate(); + foreach (GUITextBlock labelText in labelTexts) + { + labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); + } + }; + + button.SelectedColor = button.Color; + + selectedTab = TabEnum.All; + } + + public void UpdateOrAddServerInfo(ServerInfo serverInfo) + { + GUIComponent existingElement = serverList.Content.FindChild(d => + d.UserData is ServerInfo existingServerInfo && + existingServerInfo.Endpoint == serverInfo.Endpoint); + if (existingElement == null) + { + AddToServerList(serverInfo); + } + else + { + existingElement.UserData = serverInfo; + } + } + + public void AddToRecentServers(ServerInfo info) + { + if (info.Endpoint.Address.IsLocalHost) { return; } + tabs[TabEnum.Recent].AddOrUpdate(info); + tabs[TabEnum.Recent].Save(); + } + + public bool IsFavorite(ServerInfo info) + => tabs[TabEnum.Favorites].Contains(info); + + public void AddToFavoriteServers(ServerInfo info) + { + tabs[TabEnum.Favorites].AddOrUpdate(info); + tabs[TabEnum.Favorites].Save(); + } + + public void RemoveFromFavoriteServers(ServerInfo info) + { + tabs[TabEnum.Favorites].Remove(info); + tabs[TabEnum.Favorites].Save(); + } + + private bool SortList(GUIButton button, object obj) + { + if (!(obj is ColumnLabel sortBy)) { return false; } + SortList(sortBy, toggle: true); + return true; + } + + private void SortList(ColumnLabel sortBy, bool toggle) + { + if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + + sortedBy = sortBy; + + var arrowUp = button.GetChildByUserData("arrowup"); + var arrowDown = button.GetChildByUserData("arrowdown"); + + //disable arrow buttons in other labels + foreach (var child in button.Parent.Children) + { + if (child != button) + { + child.GetChildByUserData("arrowup").Visible = false; + child.GetChildByUserData("arrowdown").Visible = false; + } + } + + bool ascending = arrowUp.Visible; + if (toggle) + { + ascending = !ascending; + } + + arrowUp.Visible = ascending; + arrowDown.Visible = !ascending; + serverList.Content.RectTransform.SortChildren((c1, c2) => + { + if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } + if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } + + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + }); + } + + public override void Select() + { + base.Select(); + + Steamworks.SteamMatchmaking.ResetActions(); + + selectedTab = TabEnum.All; + GameMain.ServerListScreen.LoadServerFilters(); + if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) + { + var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("FilterOffensiveServersPrompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => + { + filterOffensive.Selected = true; + filterOffensivePrompt.Close(); + return true; + }; + filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; + + var config = GameSettings.CurrentConfig; + config.ShowOffensiveServerPrompt = false; + GameSettings.SetCurrentConfig(config); + } + + if (GameMain.Client != null) + { + GameMain.Client.Quit(); + GameMain.Client = null; + } + + RefreshServers(); + } + + public override void Deselect() + { + base.Deselect(); + GameSettings.SaveCurrentConfig(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + + UpdateFriendsList(); + panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; + + if (PlayerInput.PrimaryMouseButtonClicked()) + { + friendPopup = null; + if (friendsDropdown != null && friendsDropdownButton != null && + !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && + !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) + { + friendsDropdown.Visible = false; + } + } + } + + private void FilterServers() + { + RemoveMsgFromServerList(MsgUserData.NoMatchingServers); + foreach (GUIComponent child in serverList.Content.Children) + { + if (!(child.UserData is ServerInfo serverInfo)) { continue; } + child.Visible = ShouldShowServer(serverInfo); + } + + if (serverList.Content.Children.All(c => !c.Visible)) + { + PutMsgInServerList(MsgUserData.NoMatchingServers); + } + serverList.UpdateScrollBarSize(); + } + + private bool ShouldShowServer(ServerInfo serverInfo) + { +#if !DEBUG + //never show newer versions + //(ignore revision number, it doesn't affect compatibility) + if (ToolBox.VersionNewerIgnoreRevision(GameMain.Version, serverInfo.GameVersion)) + { + return false; + } +#endif + + if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } + + if (filterSameVersion.Selected) + { + if (!NetworkMember.IsCompatible(serverInfo.GameVersion, GameMain.Version)) { return false; } + } + if (filterPassword.Selected) + { + if (serverInfo.HasPassword) { return false; } + } + if (filterFull.Selected) + { + if (serverInfo.PlayerCount >= serverInfo.MaxPlayers) { return false; } + } + if (filterEmpty.Selected) + { + if (serverInfo.PlayerCount <= 0) { return false; } + } + if (filterOffensive.Selected) + { + if (ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) { return false; } + } + + if (filterKarmaValue != TernaryOption.Any) + { + if (serverInfo.KarmaEnabled != (filterKarmaValue == TernaryOption.Enabled)) { return false; } + } + if (filterFriendlyFireValue != TernaryOption.Any) + { + if (serverInfo.FriendlyFireEnabled != (filterFriendlyFireValue == TernaryOption.Enabled)) { return false; } + } + if (filterTraitorValue != TernaryOption.Any) + { + if ((serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) != (filterTraitorValue == TernaryOption.Enabled)) + { + return false; + } + } + if (filterVoipValue != TernaryOption.Any) + { + if (serverInfo.VoipEnabled != (filterVoipValue == TernaryOption.Enabled)) { return false; } + } + if (filterModdedValue != TernaryOption.Any) + { + if (serverInfo.IsModded != (filterModdedValue == TernaryOption.Enabled)) { return false; } + } + + foreach (GUITickBox tickBox in playStyleTickBoxes.Values) + { + var playStyle = (PlayStyle)tickBox.UserData; + if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) + { + return false; + } + } + + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) + { + var gameMode = (Identifier)tickBox.UserData; + if (!tickBox.Selected && !serverInfo.GameMode.IsEmpty && serverInfo.GameMode == gameMode) + { + return false; + } + } + + return true; + } + + private void ShowDirectJoinPrompt() + { + var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", + new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); + msgBox.Content.ChildAnchor = Anchor.TopCenter; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter) + { + IgnoreLayoutGroups = false, + Stretch = true, + RelativeSpacing = 0.05f + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); + var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); + + content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); + content.RectTransform.IsFixedSize = true; + msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f)); + + var okButton = msgBox.Buttons[0]; + okButton.Enabled = false; + okButton.OnClicked = (btn, userdata) => + { + if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } + JoinServer(endpoint, ""); + msgBox.Close(); + return false; + }; + + var favoriteButton = msgBox.Buttons[1]; + favoriteButton.Enabled = false; + favoriteButton.OnClicked = (button, userdata) => + { + if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } + + var serverInfo = new ServerInfo(endpoint) + { + ServerName = "Server", + GameVersion = GameMain.Version + }; + + var serverFrame = serverList.Content.FindChild(d => + d.UserData is ServerInfo info + && info.Equals(serverInfo)); + + if (serverFrame != null) + { + serverInfo = (ServerInfo)serverFrame.UserData; + } + else + { + AddToServerList(serverInfo); + } + + AddToFavoriteServers(serverInfo); + + selectedTab = TabEnum.Favorites; + FilterServers(); + + #warning Interface with server providers to get up-to-date info on the given server + + msgBox.Close(); + return false; + }; + + var cancelButton = msgBox.Buttons[2]; + cancelButton.OnClicked = msgBox.Close; + + endpointBox.OnTextChanged += (textBox, text) => + { + okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); + return true; + }; + } + + private bool JoinFriend(GUIButton button, object userdata) + { + if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } + + GameMain.Instance.ConnectCommand = info.ConnectCommand; + return false; + } + + private bool OpenFriendPopup(GUIButton button, object userdata) + { + if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } + + if (info.IsInServer + && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } + && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + { + const int framePadding = 5; + + friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); + + var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), nameAndEndpoint.ServerName ?? "[Unnamed]"); + serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); + + var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) + { + UserData = info + }; + joinButton.OnClicked = JoinFriend; + joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); + + Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); + int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; + int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); + + // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. + Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); + + // Add padding to the server name to match the padding on the button text. + serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); + + // Get the dimensions of the text we want to show, plus the extra padding we added. + Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); + + // Now determine how large the parent frame has to be to exactly fit our two controls. + Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); + + var popupPos = PlayerInput.MousePosition.ToPoint(); + if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) + { + // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. + popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; + } + + // Apply the size and position changes. + friendPopup.RectTransform.NonScaledSize = frameDims; + friendPopup.RectTransform.RelativeOffset = Vector2.Zero; + friendPopup.RectTransform.AbsoluteOffset = popupPos; + + joinButton.RectTransform.NonScaledSize = finalButtonSize; + + friendPopup.RectTransform.RecalculateChildren(true); + friendPopup.RectTransform.SetPosition(Anchor.TopLeft); + } + + return false; + } + + public enum AvatarSize + { + Small, + Medium, + Large + } + + private void UpdateFriendsList() + { + if (friendsListUpdateTime > Timing.TotalTime) { return; } + friendsListUpdateTime = Timing.TotalTime + 5.0; + + float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; + + friendsDropdown ??= new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) + { + OutlineColor = Color.Black, + Visible = false + }; + friendsDropdown.ClearChildren(); + + var avatarSize = friendsButtonHolder.RectTransform.Rect.Height switch + { + var h when h <= 24 => AvatarSize.Small, + var h when h <= 48 => AvatarSize.Medium, + _ => AvatarSize.Large + }; + + FriendInfo[] friends = friendProvider.RetrieveFriends(); + + foreach (var friend in friends) + { + int existingIndex = friendsList.FindIndex(f => f.Id == friend.Id); + if (existingIndex >= 0) + { + friend.Avatar = friend.Avatar.Fallback(friendsList[existingIndex].Avatar); + } + + if (friend.Avatar.IsNone()) + { + friendProvider.RetrieveAvatar(friend, avatarSize); + } + } + + friendsList.Clear(); friendsList.AddRange(friends.OrderByDescending(f => f.CurrentStatus)); + + friendsButtonHolder.ClearChildren(); + + if (friendsList.Count > 0) + { + friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") + { + OnClicked = (button, udt) => + { + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); + friendsDropdown.RectTransform.RecalculateChildren(true); + friendsDropdown.Visible = !friendsDropdown.Visible; + return false; + } + }; + } + else + { + friendsDropdownButton = null; + friendsDropdown.Visible = false; + } + + for (int i = 0; i < friendsList.Count; i++) + { + var friend = friendsList[i]; + + if (i < 5) + { + string style = friend.IsPlayingBarotrauma + ? "GUIButtonFriendPlaying" + : "GUIButtonFriendNotPlaying"; + + var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) + { + UserData = friend, + OnClicked = OpenFriendPopup + }; + guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; + + if (friend.Avatar.TryUnwrap(out Sprite sprite)) + { + new GUICustomComponent(new RectTransform(Vector2.One, guiButton.RectTransform, Anchor.Center), + onDraw: (sb, component) => + { + var destinationRect = component.Rect; + destinationRect.Inflate(-GUI.IntScale(4), -GUI.IntScale(4)); + sb.Draw(sprite.Texture, destinationRect, Color.White); + + if (!GUI.IsMouseOn(guiButton)) + { + return; + } + + sb.End(); + sb.Begin( + SpriteSortMode.Deferred, + blendState: BlendState.Additive, + samplerState: GUI.SamplerState, + rasterizerState: GameMain.ScissorTestEnable); + sb.Draw(sprite.Texture, destinationRect, Color.White * 0.5f); + sb.End(); + sb.Begin( + SpriteSortMode.Deferred, + samplerState: GUI.SamplerState, + rasterizerState: GameMain.ScissorTestEnable); + }) { CanBeFocused = false }; + } + } + + var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); + if (friend.Avatar.TryUnwrap(out var avatar)) + { + GUIImage guiImage = + new GUIImage( + new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, + scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) }, + avatar, null, true); + } + + var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) + { + Font = GUIStyle.SmallFont + }; + if (friend.IsPlayingBarotrauma) { textBlock.TextColor = GUIStyle.Green; } + if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } + + if (friend.IsInServer) + { + var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") + { + UserData = friend, + OnClicked = JoinFriend + }; + } + } + + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); + friendsDropdown.RectTransform.RecalculateChildren(true); + + friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; + } + + private void RemoveMsgFromServerList() + { + serverList.Content.Children + .Where(c => c.UserData is MsgUserData) + .ForEachMod(serverList.Content.RemoveChild); + } + + private void RemoveMsgFromServerList(MsgUserData userData) + { + serverList.Content.RemoveChild(serverList.Content.FindChild(userData)); + } + + private void PutMsgInServerList(MsgUserData userData) + { + RemoveMsgFromServerList(); + new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), + TextManager.Get(userData.ToString()), textAlignment: Alignment.Center) + { + CanBeFocused = false, + UserData = userData + }; + } + + private void RefreshServers() + { + lastRefreshTime = DateTime.Now; + serverProvider.Cancel(); + currentServerDataRecvCallbackObj = null; + + PingUtils.QueryPingData(); + + tabs[TabEnum.All].Clear(); + serverList.ClearChildren(); + serverPreview.Content.ClearChildren(); + panelAnimator.RightEnabled = false; + joinButton.Enabled = false; + selectedServer = null; + + if (selectedTab == TabEnum.All) + { + PutMsgInServerList(MsgUserData.RefreshingServerList); + } + else + { + var servers = tabs[selectedTab].Servers.ToArray(); + foreach (var server in servers) + { + server.Ping = Option.None(); + AddToServerList(server, skipPing: true); + } + + if (!servers.Any()) + { + PutMsgInServerList(MsgUserData.NoServers); + return; + } + } + + var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks(); + serverProvider.RetrieveServers(onServerDataReceived, onQueryCompleted); + } + + private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo) + { + bool matches(GUIComponent c) + => c.UserData is ServerInfo info + && info.Equals(serverInfo); + +#if DEBUG + if (serverList.Content.Children.Count(matches) > 1) + { + DebugConsole.ThrowError($"There are several entries in the server list for endpoint {serverInfo.Endpoint}"); + } +#endif + + return serverList.Content.FindChild(matches); + } + + private object currentServerDataRecvCallbackObj = null; + private (Action OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks() + { + var uniqueObject = new object(); + currentServerDataRecvCallbackObj = uniqueObject; + + bool shouldRunCallback() + { + // If currentServerDataRecvCallbackObj != uniqueObject, then one of the following happened: + // - The query this call is associated to was meant to be over + // - Another query was started before the one associated to this call was finished + // In either case, do not add the received info to the server list. + return ReferenceEquals(currentServerDataRecvCallbackObj, uniqueObject); + } + + return ( + serverInfo => + { + if (!shouldRunCallback()) { return; } + + if (selectedTab == TabEnum.All) + { + AddToServerList(serverInfo); + } + else + { + if (FindFrameMatchingServerInfo(serverInfo) == null) { return; } + UpdateServerInfoUI(serverInfo); + PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); + } + }, + () => + { + if (shouldRunCallback()) { ServerQueryFinished(); } + } + ); + } + + private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) + { + RemoveMsgFromServerList(MsgUserData.RefreshingServerList); + RemoveMsgFromServerList(MsgUserData.NoServers); + var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, + style: "ListBoxElement") + { + UserData = serverInfo + }; + new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = false + }; + UpdateServerInfoUI(serverInfo); + if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } + + SortList(sortedBy, toggle: false); + FilterServers(); + } + + private void UpdateServerInfoUI(ServerInfo serverInfo) + { + var serverFrame = FindFrameMatchingServerInfo(serverInfo); + if (serverFrame == null) { return; } + + serverFrame.UserData = serverInfo; + + serverFrame.ToolTip = ""; + var serverContent = serverFrame.Children.First() as GUILayoutGroup; + serverContent.ClearChildren(); + + Dictionary sections = new Dictionary(); + foreach (ColumnLabel label in Enum.GetValues(typeof(ColumnLabel))) + { + sections[label] = + new GUIFrame( + new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform), + style: null); + } + + void errorTooltip(RichString toolTip) + { + sections.Values.ForEach(c => + { + c.CanBeFocused = false; + c.Children.First().CanBeFocused = false; + }); + serverFrame.ToolTip = toolTip; + } + + RectTransform columnRT(ColumnLabel label, float scale = 0.95f) + => new RectTransform(Vector2.One * scale, sections[label].RectTransform, Anchor.Center); + + void sectionTooltip(ColumnLabel label, RichString toolTip) + { + var section = sections[label]; + section.CanBeFocused = true; + section.ToolTip = toolTip; + } + + var compatibleBox = new GUITickBox(columnRT(ColumnLabel.ServerListCompatible), label: "") + { + CanBeFocused = false, + Selected = + NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion), + UserData = "compatible" + }; + + var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox") + { + Selected = serverInfo.HasPassword, + UserData = "password", + CanBeFocused = false + }; + sectionTooltip(ColumnLabel.ServerListHasPassword, + TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword")); + + var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName), +#if DEBUG + $"[{serverInfo.Endpoint.GetType().Name}] " + +#endif + serverInfo.ServerName, + style: "GUIServerListTextBox") { CanBeFocused = false }; + + if (serverInfo.IsModded) + { + serverName.TextColor = GUIStyle.ModdedServerColor; + } + + new GUITickBox(columnRT(ColumnLabel.ServerListRoundStarted), label: "") + { + Selected = serverInfo.GameStarted, + CanBeFocused = false + }; + sectionTooltip(ColumnLabel.ServerListRoundStarted, + TextManager.Get(serverInfo.GameStarted ? "ServerListRoundStarted" : "ServerListRoundNotStarted")); + + var serverPlayers = new GUITextBlock(columnRT(ColumnLabel.ServerListPlayers), + $"{serverInfo.PlayerCount}/{serverInfo.MaxPlayers}", style: "GUIServerListTextBox", textAlignment: Alignment.Right) + { + ToolTip = TextManager.Get("ServerListPlayers") + }; + + var serverPingText = new GUITextBlock(columnRT(ColumnLabel.ServerListPing), "?", + style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) + { + ToolTip = TextManager.Get("ServerListPing") + }; + + if (serverInfo.Ping.TryUnwrap(out var ping)) + { + serverPingText.Text = ping.ToString(); + serverPingText.TextColor = GetPingTextColor(ping); + } + else + { + serverPingText.Text = "?"; + serverPingText.TextColor = Color.DarkRed; + } + + if (!serverInfo.Checked) + { + errorTooltip(TextManager.Get("ServerOffline")); + serverName.TextColor *= 0.8f; + serverPlayers.TextColor *= 0.8f; + } + else if (!serverInfo.ContentPackages.Any()) + { + compatibleBox.Selected = false; + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), + " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) + { + ToolTip = TextManager.Get("ServerListUnknownContentPackage") + }; + } + else if (!compatibleBox.Selected) + { + LocalizedString toolTip = ""; + if (serverInfo.GameVersion != GameMain.Version) + { + toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); + } + + int maxIncompatibleToList = 10; + List incompatibleModNames = new List(); + foreach (var contentPackage in serverInfo.ContentPackages) + { + bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(cp => cp.Hash.StringRepresentation == contentPackage.Hash); + if (listAsIncompatible) + { + incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", + ("[name]", contentPackage.Name), + ("[hash]", Md5Hash.GetShortHash(contentPackage.Hash)))); + } + } + if (incompatibleModNames.Any()) + { + toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); + if (incompatibleModNames.Count > maxIncompatibleToList) + { + toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); + } + } + errorTooltip(toolTip); + + serverName.TextColor *= 0.5f; + serverPlayers.TextColor *= 0.5f; + } + else + { + LocalizedString toolTip = ""; + foreach (var contentPackage in serverInfo.ContentPackages) + { + if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) + { + if (toolTip != "") { toolTip += "\n"; } + toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", contentPackage.Name); + break; + } + } + errorTooltip(toolTip); + } + + foreach (var section in sections.Values) + { + var child = section.Children.First(); + child.RectTransform.ScaleBasis + = child is GUITextBlock ? ScaleBasis.Normal : ScaleBasis.BothHeight; + } + + // The next twenty-something lines are an optimization. + // The issue is that the serverlist has a ton of text elements, + // and resizing all of them is extremely expensive. However, since + // you don't see most of them most of the time, it makes sense to + // just resize them lazily based on when you actually can see them. + // That would entail a UI refactor of some kind, and I don't want to + // do that just yet, so here's a hack instead! + bool isDirty = true; + void markAsDirty() => isDirty = true; + serverContent.GetAllChildren().ForEach(c => + { + c.RectTransform.ResetSizeChanged(); + c.RectTransform.SizeChanged += markAsDirty; + }); + new GUICustomComponent(new RectTransform(Vector2.Zero, serverContent.RectTransform), onUpdate: (_, __) => + { + if (serverFrame.MouseRect.Height <= 0 || !isDirty) { return; } + serverContent.GetAllChildren().ForEach(c => + { + switch (c) + { + case GUITextBlock textBlock: + textBlock.SetTextPos(); + break; + case GUITickBox tickBox: + tickBox.ResizeBox(); + break; + } + }); + serverName.Text = ToolBox.LimitString(serverInfo.ServerName, serverName.Font, serverName.Rect.Width); + isDirty = false; + }); + // Hacky optimization ends here + + serverContent.Recalculate(); + + if (tabs[TabEnum.Favorites].Contains(serverInfo)) + { + AddToFavoriteServers(serverInfo); + } + + SortList(sortedBy, toggle: false); + FilterServers(); + } + + private void ServerQueryFinished() + { + currentServerDataRecvCallbackObj = null; + if (!serverList.Content.Children.Any(c => c.UserData is ServerInfo)) + { + PutMsgInServerList(MsgUserData.NoServers); + } + else if (serverList.Content.Children.All(c => !c.Visible)) + { + PutMsgInServerList(MsgUserData.NoMatchingServers); + } + } + + public void JoinServer(Endpoint endpoint, string serverName) + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + return; + } + + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; + GameSettings.SaveCurrentConfig(); + +#if !DEBUG + try + { +#endif + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(GetDefaultUserName()), endpoint, serverName, Option.None()); +#if !DEBUG + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to start the client", e); + } +#endif + } + + private Color GetPingTextColor(int ping) + { + if (ping < 0) { return Color.DarkRed; } + return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + graphics.Clear(Color.CornflowerBlue); + + GameMain.TitleScreen.DrawLoadingText = false; + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); + + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + + GUI.Draw(Cam, spriteBatch); + + spriteBatch.End(); + } + + public override void AddToGUIUpdateList() + { + menu.AddToGUIUpdateList(); + friendPopup?.AddToGUIUpdateList(); + friendsDropdown?.AddToGUIUpdateList(); + } + + public void StoreServerFilters() + { + foreach (KeyValuePair filterBox in filterTickBoxes) + { + ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); + } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); + } + } + + public void LoadServerFilters() + { + XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + foreach (KeyValuePair filterBox in filterTickBoxes) + { + filterBox.Value.Selected = + ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); + } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + TernaryOption ternaryOption = + ServerListFilters.Instance.GetAttributeEnum( + ternaryFilter.Key, + (TernaryOption)ternaryFilter.Value.SelectedData); + + var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); + ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); + } + } + + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 3178cff7e..9bff7dac0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1547,6 +1547,8 @@ namespace Barotrauma GUI.ForceMouseOn(null); + if (ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); } + MapEntityPrefab.Selected = null; saveFrame = null; @@ -1797,25 +1799,14 @@ namespace Barotrauma { Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); - void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) + static string getExistingFilePath(ContentPackage package, string fileName) { - filePath = filePath.CleanUpPath(); - packagePath = packagePath.CleanUpPath(); - string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); - if (filePath.StartsWith(packageDir)) + if (Submarine.MainSub?.Info == null) { return null; } + if (package.Files.Any(f => f.Path == MainSub.Info.FilePath && Path.GetFileName(f.Path.Value) == fileName)) { - filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + return MainSub.Info.FilePath; } - if (!modProject.Files.Any(f => f.Type == subFileType && - f.Path == filePath)) - { - var newFile = ModProject.File.FromPath(filePath, subFileType); - modProject.AddFile(newFile); - } - - using var _ = Validation.SkipInDebugBuilds(); - modProject.DiscardHashAndInstallTime(); - modProject.Save(packagePath); + return null; } if (!GameMain.DebugDraw) @@ -1861,101 +1852,139 @@ namespace Barotrauma #if !DEBUG throw new InvalidOperationException("Cannot save to Vanilla package"); #endif - savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch - { - SubmarineType.Player => "Content/Submarines/{0}", - SubmarineType.Outpost => "Content/Map/Outposts/{0}", - SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... - SubmarineType.Wreck => "Content/Map/Wrecks/{0}", - SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", - SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", - SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", - _ => throw new InvalidOperationException() - }, savePath); + savePath = + getExistingFilePath(packageToSaveTo, savePath) ?? + string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch + { + SubmarineType.Player => "Content/Submarines/{0}", + SubmarineType.Outpost => "Content/Map/Outposts/{0}", + SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... + SubmarineType.Wreck => "Content/Map/Wrecks/{0}", + SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", + SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", + SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", + _ => throw new InvalidOperationException() + }, savePath); modProject.ModVersion = ""; } else { - savePath = Path.Combine(packageToSaveTo.Dir, savePath); + string existingFilePath = getExistingFilePath(packageToSaveTo, savePath); + //if we're trying to save a sub that's already included in the package with the same name as before, save directly in the same path + if (existingFilePath != null) + { + savePath = existingFilePath; + } + //otherwise make sure we're not trying to overwrite another sub in the same package + else + { + savePath = Path.Combine(packageToSaveTo.Dir, savePath); + if (File.Exists(savePath)) + { + var verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + verification.Buttons[0].OnClicked = (_, _) => + { + addSubAndSave(modProject, savePath, fileListPath); + verification.Close(); + return true; + }; + verification.Buttons[1].OnClicked = verification.Close; + return false; + } + } } - addSubAndSaveModProject(modProject, savePath, fileListPath); - } - else if (MainSub?.Info?.FilePath != null - && MainSub.Info.Name != null - && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) - && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - prevSavePath = MainSub.Info.FilePath.CleanUpPath(); - ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); - if (contentPackage == null) - { - throw new InvalidOperationException($"Tried to overwrite a submarine ({name}) that's not in a local package!"); - } - ModProject modProject = new ModProject(contentPackage); - packageToSaveTo = contentPackage; - savePath = prevSavePath; - addSubAndSaveModProject(modProject, savePath, contentPackage.Path); + addSubAndSave(modProject, savePath, fileListPath); } else { savePath = Path.Combine(newLocalModDir, savePath); - ModProject modProject = new ModProject { Name = name }; - addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); - } - savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); - - if (MainSub != null) - { - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) + if (File.Exists(savePath)) { - bool savePreviewImage = true; - using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); - try - { - previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); - savePreviewImage = false; - } - MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); + new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariable("subeditor.packagealreadyexists", "[name]", name)); + return false; } else { - MainSub.TrySaveAs(savePath); + ModProject modProject = new ModProject { Name = name }; + addSubAndSave(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + } - MainSub.CheckForErrors(); - - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); - - if (savePath.StartsWith(newLocalModDir)) + void addSubAndSave(ModProject modProject, string filePath, string packagePath) + { + filePath = filePath.CleanUpPath(); + packagePath = packagePath.CleanUpPath(); + string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + if (filePath.StartsWith(packageDir)) { - ContentPackageManager.LocalPackages.Refresh(); - var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); - if (newPackage is RegularPackage regular) + filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + } + if (!modProject.Files.Any(f => f.Type == subFileType && + f.Path == filePath)) + { + var newFile = ModProject.File.FromPath(filePath, subFileType); + modProject.AddFile(newFile); + } + + using var _ = Validation.SkipInDebugBuilds(); + modProject.DiscardHashAndInstallTime(); + modProject.Save(packagePath); + + savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); + if (MainSub != null) + { + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) { - ContentPackageManager.EnabledPackages.EnableRegular(regular); - GameSettings.SaveCurrentConfig(); + bool savePreviewImage = true; + using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); + try + { + previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); + savePreviewImage = false; + } + MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } - } - if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } - SubmarineInfo.RefreshSavedSub(savePath); - if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } - MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; + else + { + MainSub.TrySaveAs(savePath); + } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { - if (sub.Type != SubmarineType.Player) { continue; } - if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + MainSub.CheckForErrors(); + + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); + + if (savePath.StartsWith(newLocalModDir)) + { + ContentPackageManager.LocalPackages.Refresh(); + var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); + if (newPackage is RegularPackage regular) + { + ContentPackageManager.EnabledPackages.EnableRegular(regular); + GameSettings.SaveCurrentConfig(); + } + } + if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } + SubmarineInfo.RefreshSavedSub(savePath); + if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } + MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; + + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + linkedSubBox.ClearChildren(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } + linkedSubBox.AddItem(sub.Name, sub); + } + subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } - subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } return false; @@ -2735,40 +2764,31 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), onUpdate: (f, component) => { - bool canCreateNewPackage = true; foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) { - contentChild.Visible = !(contentChild.UserData is ContentPackage p) - || !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase); - canCreateNewPackage &= contentChild.Visible; contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); } - - if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage) - { - GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); - newPackageListText.Text = TextManager.Get("CreateNewLocalPackage"); - } - if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage) - { - GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton"); - newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text); - } }); - packageToSaveInList.Select(0); ContentPackage ownerPkg = null; if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } foreach (var p in ContentPackageManager.LocalPackages) { - addItemToPackageToSaveList(p.Name, p); + var packageListItem = addItemToPackageToSaveList(p.Name, p); + if (p == ownerPkg) + { + var packageListIcon = packageListItem.GetChild(); + var packageListText = packageListItem.GetChild(); + GUIStyle.Apply(packageListIcon, "WorkshopMenu.EditButton"); + packageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", p.Name); + } } - - if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase)) + if (ownerPkg != null) { - packageToSaveInList.Select(ownerPkg); - packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); + var element = packageToSaveInList.Content.FindChild(ownerPkg); + element?.RectTransform.SetAsFirstChild(); } + packageToSaveInList.Select(0); var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, horizontalArea.RectTransform, Anchor.BottomRight)) @@ -3424,7 +3444,8 @@ namespace Barotrauma { if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) { - if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) + if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId) + && publishedWorkshopItemIds.Contains(workshopId.Value)) { AskLoadPublishedSub(selectedSubInfo, workshopPackage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 3040f674e..a63e7c905 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -380,6 +380,11 @@ namespace Barotrauma } LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); + if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType) + { + Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description"); + } if (toolTip.IsNullOrEmpty()) { toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 1b1aae8c0..82c294c6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -127,15 +127,22 @@ namespace Barotrauma } } - public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.texture == texture).ToList() : new List() { this }); - - public void ReloadTexture(IEnumerable spritesToUpdate) + public void ReloadTexture() { + var oldTexture = texture; texture.Dispose(); texture = TextureLoader.FromFile(FilePath.Value, Compress); - foreach (Sprite sprite in spritesToUpdate) + Identifier pathKey = FullPath.ToIdentifier(); + if (textureRefCounts.ContainsKey(pathKey)) { - sprite.texture = texture; + textureRefCounts[pathKey].Texture = texture; + } + foreach (Sprite sprite in LoadedSprites) + { + if (sprite.texture == oldTexture) + { + sprite.texture = texture; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 4a25ebe63..d572c3797 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -70,7 +70,9 @@ namespace Barotrauma.Steam (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); return (await Task.WhenAll(outOfDatePackages.Where(p => !p.IsUpToDate) - .Select(async p => await SteamManager.Workshop.GetItem(p.Package.SteamWorkshopId)))) + .Select(p => p.Package.UgcId) + .OfType() + .Select(async id => await SteamManager.Workshop.GetItem(id.Value)))) .Where(p => p.HasValue).Select(p => p ?? default).ToArray(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 7701c1469..54cab99e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -1,7 +1,5 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -102,7 +100,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId))); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); @@ -152,275 +150,5 @@ namespace Barotrauma.Steam } }); } - - public static bool GetServers(Action addToServerList, Action serverQueryFinished) - { - if (!IsInitialized) { return false; } - - int doneTasks = 0; - void taskDone() - { - doneTasks++; - if (doneTasks >= 2) - { - serverQueryFinished?.Invoke(); - serverQueryFinished = null; - } - } - - - Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => - { - DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); - }; - - TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), - (t) => - { - Steamworks.Dispatch.OnDebugCallback = null; - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); - taskDone(); - return; - } - var lobbies = ((Task>)t).Result; - if (lobbies != null) - { - foreach (var lobby in lobbies) - { - if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } - - ServerInfo serverInfo = new ServerInfo - { - 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; - - AssignLobbyDataToServerInfo(lobby, serverInfo); - - addToServerList(serverInfo); - } - } - taskDone(); - }); - - Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); - void onServer(Steamworks.Data.ServerInfo info, bool responsive) - { - if (string.IsNullOrEmpty(info.Name)) { return; } - - ServerInfo serverInfo = new ServerInfo - { - ServerName = info.Name, - HasPassword = info.Passworded, - Endpoint = new LidgrenEndpoint(info.Address, info.ConnectionPort), - PlayerCount = info.Players, - MaxPlayers = info.MaxPlayers, - RespondedToSteamQuery = responsive - }; - - if (responsive) - { - TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); - return; - } - - var rules = ((Task>)t).Result; - AssignServerRulesToServerInfo(rules, serverInfo); - - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - }); - } - else - { - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - } - - } - serverQuery.OnResponsiveServer += (info) => onServer(info, true); - serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); - - TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), - (t) => - { - serverQuery.Dispose(); - taskDone(); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); - return; - } - }); - - return true; - } - - public static async Task> LobbyQueryRequest() - { - List allLobbies = new List(); - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() - .FilterDistanceWorldwide() - .WithMaxResults(50); - //steamworks seems to unable to retrieve more than 50 - //lobbies per request; to work around this, we'll make - //up to 10 requests, asking to ignore all previous results - //in each subsequent request - for (int i = 0; i < 10; i++) - { - Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); - if (lobbies == null) { break; } - foreach (var l in lobbies) - { - lobbyQuery = lobbyQuery - .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); - } - allLobbies.AddRange(lobbies); - } - - //make sure all returned lobbies are distinct, don't want any duplicates here - return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); - } - - public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - serverInfo.ServerMessage = lobby.GetData("message"); - serverInfo.GameVersion = lobby.GetData("version"); - - serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); - serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - - string workshopIdData = lobby.GetData("contentpackageid"); - if (!string.IsNullOrEmpty(workshopIdData)) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); - } - else - { - string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - - serverInfo.AllowSpectating = getLobbyBool("allowspectating"); - serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); - serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); - serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); - serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); - if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - - serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier(); - if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - - string pingLocation = lobby.GetData("pinglocation"); - if (!string.IsNullOrEmpty(pingLocation)) - { - serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); - } - - bool? getLobbyBool(string key) - { - string data = lobby.GetData(key); - if (string.IsNullOrEmpty(data)) { return null; } - return data == "True" || data == "true"; - } - } - - public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - if (rules == null) { return; } - - if (rules.ContainsKey("message")) { serverInfo.ServerMessage = rules["message"]; } - if (rules.ContainsKey("version")) { serverInfo.GameVersion = rules["version"]; } - - if (rules.ContainsKey("playercount")) - { - if (int.TryParse(rules["playercount"], out int playerCount)) { serverInfo.PlayerCount = playerCount; } - } - - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - if (rules.ContainsKey("contentpackage")) { serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); } - if (rules.ContainsKey("contentpackagehash")) { serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); } - if (rules.ContainsKey("contentpackageid")) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); - } - else if (rules.ContainsKey("contentpackageurl")) - { - string[] workshopUrls = rules["contentpackageurl"].Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - if (rules.ContainsKey("modeselectionmode")) - { - if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - } - if (rules.ContainsKey("subselectionmode")) - { - if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - } - if (rules.ContainsKey("allowspectating")) { serverInfo.AllowSpectating = rules["allowspectating"] == "True"; } - if (rules.ContainsKey("allowrespawn")) { serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; } - if (rules.ContainsKey("voicechatenabled")) { serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; } - if (rules.ContainsKey("friendlyfireenabled")) { serverInfo.FriendlyFireEnabled = rules["friendlyfireenabled"] == "True"; } - if (rules.ContainsKey("karmaenabled")) { serverInfo.KarmaEnabled = rules["karmaenabled"] == "True"; } - if (rules.ContainsKey("traitors")) - { - if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - } - - if (rules.ContainsKey("gamestarted")) { serverInfo.GameStarted = rules["gamestarted"] == "True"; } - if (rules.ContainsKey("gamemode")) - { - serverInfo.GameMode = rules["gamemode"].ToIdentifier(); - } - if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) - { - serverInfo.PlayStyle = playStyle; - } - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index a21ef24c9..370b47f3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Steam throw new Exception("Expected Workshop package"); } - if (contentPackage.SteamWorkshopId == 0) + if (!contentPackage.UgcId.TryUnwrap(out var ugcId) || !(ugcId is SteamWorkshopId workshopId)) { throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}"); } @@ -210,7 +210,7 @@ namespace Barotrauma.Steam string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; if (File.Exists(newPath) || Directory.Exists(newPath)) { - newPath += $"_{contentPackage.SteamWorkshopId}"; + newPath += $"_{workshopId.Value}"; } if (File.Exists(newPath) || Directory.Exists(newPath)) @@ -226,7 +226,7 @@ namespace Barotrauma.Steam RefreshLocalMods(); - return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId); + return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.UgcId == contentPackage.UgcId); } private struct InstallWaiter @@ -266,7 +266,10 @@ namespace Barotrauma.Steam { NukeDownload(workshopItem); var toUninstall - = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + = ContentPackageManager.WorkshopPackages.Where(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == workshopItem.Id) .ToHashSet(); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); @@ -296,7 +299,10 @@ namespace Barotrauma.Steam return; } else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id) + && !ContentPackageManager.WorkshopPackages.Any(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId workshopId + && workshopId.Value == id) && !InstallTaskCounter.IsInstalling(id)) { TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 32482cc76..cce1627d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -35,7 +35,12 @@ namespace Barotrauma.Steam memSubscribedModCount = numSubscribedMods; var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); - var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); + var installedIds = ContentPackageManager.WorkshopPackages + .Select(p => p.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value) + .ToHashSet(); foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) { Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); @@ -514,7 +519,9 @@ namespace Barotrauma.Steam private void PrepareToShowModInfo(ContentPackage mod) { - TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + if (!mod.UgcId.TryUnwrap(out var ugcId) + || !(ugcId is SteamWorkshopId workshopId)) { return; } + TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), t => { if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } @@ -592,7 +599,12 @@ namespace Barotrauma.Steam isEnabled: true, onSelected: () => { - TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + var workshopIds = selectedMods + .Select(m => m.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value); + TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(workshopIds.Select(SteamManager.Workshop.GetItem)), t => { if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } @@ -672,7 +684,7 @@ namespace Barotrauma.Steam infoButton.Enabled = false; } TaskPool.AddIfNotFound( - $"DetermineUpdateRequired{mod.SteamWorkshopId}", + $"DetermineUpdateRequired{mod.UgcId}", mod.IsUpToDate(), t => { @@ -725,6 +737,8 @@ namespace Barotrauma.Steam { var mod = child.UserData as RegularPackage; if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } + if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; } + if (!(ugcId is SteamWorkshopId workshopId)) { continue; } var btn = child.GetChild()?.GetAllChildren().Last(); if (btn is null) { continue; } @@ -732,11 +746,11 @@ namespace Barotrauma.Steam btn.ApplyStyle( GUIStyle.GetComponentStyle( - ids.Contains(mod.SteamWorkshopId) + ids.Contains(workshopId.Value) ? "WorkshopMenu.PublishedIcon" : "WorkshopMenu.DownloadedIcon")); btn.ToolTip = TextManager.Get( - ids.Contains(mod.SteamWorkshopId) + ids.Contains(workshopId.Value) ? "PublishedWorkshopMod" : "DownloadedWorkshopMod"); btn.HoverCursor = CursorState.Default; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 0d401f95a..17a677d57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Steam (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems .Select(item => (item, (ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p - => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id))) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == item.Id))) //Sort the pairs by last local edit time if available .OrderBy(t => t.Item2 == null) .ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime) @@ -241,7 +241,9 @@ namespace Barotrauma.Steam //Get mods that haven't been published and add them to the list var unpublishedMods = ContentPackageManager.LocalPackages - .Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId)) + .Where(p => + !p.TryExtractSteamWorkshopId(out var workshopId) + || !publishedItems.Any(item => item.WorkshopItem.Id == workshopId.Value)) .OrderByDescending(getEditTime).ToArray(); if (unpublishedMods.Any()) @@ -556,7 +558,9 @@ namespace Barotrauma.Steam taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; var contentPackage - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => + p.TryExtractSteamWorkshopId(out var workshopId) + && workshopId.Value == workshopItem.Id); var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); @@ -619,7 +623,7 @@ namespace Barotrauma.Steam if (contentPackage != null) { TaskPool.AddIfNotFound( - $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", + $"DetermineUpdateRequired{contentPackage.UgcId}", contentPackage.IsUpToDate(), t => { @@ -652,7 +656,9 @@ namespace Barotrauma.Steam if (contentPackage != null && !ContentPackageManager.WorkshopPackages.Contains(contentPackage) - && ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + && ContentPackageManager.WorkshopPackages.Any(p => + p.TryExtractSteamWorkshopId(out var workshopId) + && workshopId.Value == workshopItem.Id)) { updateButton.Visible = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index df5e42de0..daec8758f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Extensions; +using Barotrauma.Steam; using Microsoft.Xna.Framework; namespace Barotrauma @@ -49,21 +50,18 @@ namespace Barotrauma case ModType.Workshop: { var id = element.GetAttributeUInt64("id", 0); - var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); - if (id != 0 && pkg != null) - { - addPkg(pkg); - } + if (id == 0) { continue; } + var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => + p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == id); + if (pkg != null) { addPkg(pkg); } } break; case ModType.Local: { var name = element.GetAttributeString("name", ""); + if (name.IsNullOrEmpty()) { continue; } var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); - if (!name.IsNullOrEmpty() && pkg != null) - { - addPkg(pkg); - } + if (pkg != null) { addPkg(pkg); } } break; } @@ -115,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); + pkgElem.SetAttributeValue("id", pkg.UgcId.ToString()); break; case ModType.Local: pkgElem.SetAttributeValue("name", pkg.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 6d6cc04ed..61d849dbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -95,6 +95,9 @@ namespace Barotrauma.Steam SelectTab(Tab.Publish); } + private static bool PackageMatchesItem(ContentPackage p, Steamworks.Ugc.Item workshopItem) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == workshopItem.Id; + private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); @@ -105,18 +108,19 @@ namespace Barotrauma.Steam childAnchor: Anchor.TopCenter); Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default; + ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package) ? package - : ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + : ContentPackageManager.LocalPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); ContentPackage? workshopPackage - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); if (localPackage is null) { new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null); //Local copy does not exist; check for Workshop copy bool workshopCopyExists = - ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem)); new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform), TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"), @@ -403,7 +407,7 @@ namespace Barotrauma.Steam private IEnumerable CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) { ContentPackage? workshopCopy = - ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem)); if (workshopCopy is null) { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) @@ -417,7 +421,7 @@ namespace Barotrauma.Steam { ContentPackageManager.WorkshopPackages.Refresh(); }); - while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + while (!ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem))) { currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) ? TextManager.Get("PublishPopupInstall") @@ -426,7 +430,7 @@ namespace Barotrauma.Steam } workshopCopy = - ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackageManager.WorkshopPackages.First(p => PackageMatchesItem(p, workshopItem)); } bool localCopyMade = false; @@ -480,7 +484,7 @@ namespace Barotrauma.Steam messageBox.Buttons[0].Enabled = false; Steamworks.Ugc.PublishResult? result = null; Exception? resultException = null; - TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", + TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.UgcId})", editor.SubmitAsync(), t => { @@ -496,6 +500,8 @@ namespace Barotrauma.Steam if (result is { Success: true }) { var resultId = result.Value.FileId; + bool packageMatchesResult(ContentPackage p) + => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == resultId; Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem); while (!resultItem.IsInstalled && !downloadTask.IsCompleted) @@ -511,7 +517,7 @@ namespace Barotrauma.Steam } ContentPackage? pkgToNuke - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); + = ContentPackageManager.WorkshopPackages.FirstOrDefault(packageMatchesResult); if (pkgToNuke != null) { Directory.Delete(pkgToNuke.Dir, recursive: true); @@ -537,7 +543,7 @@ namespace Barotrauma.Steam var localModProject = new ModProject(localPackage) { - SteamWorkshopId = resultId + UgcId = Option.Some(new SteamWorkshopId(resultId)) }; localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs index f13fdd117..f2cf800d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs @@ -19,7 +19,7 @@ namespace Barotrauma public override bool Loaded => nestedStr.Loaded; public override void RetrieveValue() { - cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.Value, textScale); + cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.GetFontForStr(nestedStr.Value), textScale); UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index ce5e7b251..50cde2d74 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index fd0d561b2..a6511a7d3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 4e4bc2642..97e542512 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f60b3ea38..be1d93bfe 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 2f621c063..52c412583 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 561218a63..49893f4dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -61,8 +61,9 @@ namespace Barotrauma msg.WriteColorR8G8B8(Head.SkinColor); msg.WriteColorR8G8B8(Head.HairColor); msg.WriteColorR8G8B8(Head.FacialHairColor); - msg.WriteString(ragdollFileName); + msg.WriteString(ragdollFileName); + msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); if (Job != null) { msg.WriteUInt32(Job.Prefab.UintIdentifier); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index be7481ad2..1cf319471 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -243,6 +243,7 @@ namespace Barotrauma maxPlayers, ownerKey, steamId); + Server.StartServer(); for (int i = 0; i < CommandLineArgs.Length; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index e81939b28..138dc3cfe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System; using System.Xml.Linq; namespace Barotrauma @@ -20,19 +21,22 @@ namespace Barotrauma CharacterInfo = client.CharacterInfo; healthData = new XElement("health"); - client.Character?.CharacterHealth?.Save(healthData); - if (client.Character?.Inventory != null) + + //the character may not be controlled by the client atm, but still exist + Character character = client.Character ?? CharacterInfo?.Character; + + character?.CharacterHealth?.Save(healthData); + if (character?.Inventory != null) { itemData = new XElement("inventory"); - Character.SaveInventory(client.Character.Inventory, itemData); + Character.SaveInventory(character.Inventory, itemData); } OrderData = new XElement("orders"); - if (client.CharacterInfo != null) + if (CharacterInfo != null) { - CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); + CharacterInfo.SaveOrderData(CharacterInfo, OrderData); } - - if (client.Character?.Wallet.Save() is { } walletSave) + if (character?.Wallet.Save() is { } walletSave) { WalletData = walletSave; } @@ -118,9 +122,9 @@ namespace Barotrauma character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } - public void ApplyHealthData(Character character) + public void ApplyHealthData(Character character, Func afflictionPredicate = null) { - CharacterInfo.ApplyHealthData(character, healthData); + CharacterInfo.ApplyHealthData(character, healthData, afflictionPredicate); } public void ApplyOrderData(Character character) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index accb5e13a..502f148fc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -917,15 +917,14 @@ namespace Barotrauma foreach (var kvp in purchasedItems) { - foreach (var purchasedItemList in purchasedItems.Values) + var storeId = kvp.Key; + var purchasedItemList = kvp.Value; + foreach (var purchasedItem in purchasedItemList) { - foreach (var purchasedItem in purchasedItemList) - { - int availableQuantity = map.CurrentLocation.Stores[kvp.Key].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; - purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity); - } - } - CargoManager.PurchaseItems(kvp.Key, kvp.Value, false, sender); + int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; + purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity); + } + CargoManager.PurchaseItems(storeId, purchasedItemList, false, sender); } foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 6ba6dea70..15f4d365a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -71,6 +71,12 @@ namespace Barotrauma.Networking { expirationTime = parsedTime; } + else + { + string error = $"Failed to parse the ban duration of \"{name}\" ({separatedLine[2]}) from the legacy ban list file (text file which has now been changed to XML). Considering the ban permanent."; + DebugConsole.ThrowError(error); + GameServer.AddPendingMessageToOwner(error, ChatMessageType.Error); + } } string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : ""; @@ -156,6 +162,8 @@ namespace Barotrauma.Networking public void BanPlayer(string name, Either addressOrAccountId, string reason, TimeSpan? duration) { + if (addressOrAccountId.TryGet(out Address address) && address.IsLocalHost) { return; } + var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId); if (existingBan != null) { bannedPlayers.Remove(existingBan); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 551e32707..fdea971a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -224,27 +224,55 @@ namespace Barotrauma.Networking } } + /// + /// Reset what this client has voted for and the kick votes given to this client + /// + public void ResetVotes(bool resetKickVotes) + { + for (int i = 0; i < votes.Length; i++) + { + votes[i] = null; + } + if (resetKickVotes) + { + kickVoters.Clear(); + } + } + public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) { - this.Permissions = permissions; - this.PermittedConsoleCommands.Clear(); - this.PermittedConsoleCommands.UnionWith(permittedConsoleCommands); + Permissions = permissions; + PermittedConsoleCommands.Clear(); + PermittedConsoleCommands.UnionWith(permittedConsoleCommands); + if (Permissions.HasFlag(ClientPermissions.ManageSettings)) + { + //ensure the client has the up-to-date server settings + GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); + } } public void GivePermission(ClientPermissions permission) { - if (!this.Permissions.HasFlag(permission)) this.Permissions |= permission; + if (!Permissions.HasFlag(permission)) + { + Permissions |= permission; + if (permission.HasFlag(ClientPermissions.ManageSettings)) + { + //ensure the client has the up-to-date server settings + GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); + } + } } public void RemovePermission(ClientPermissions permission) { - this.Permissions &= ~permission; + Permissions &= ~permission; } public bool HasPermission(ClientPermissions permission) { - return this.Permissions.HasFlag(permission); + return Permissions.HasFlag(permission); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 54bbd573c..318f77ff8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Networking string resultFileName = dir.StartsWith(ContentPackage.LocalModsDir) ? $"Local_{mod.Name}" - : $"Workshop_{mod.Name}_{mod.SteamWorkshopId}"; + : $"Workshop_{mod.Name}_{(mod.UgcId.TryUnwrap(out var ugcId) ? ugcId.ToString() : "NULL")}"; resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index c3bdd614c..116010200 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -14,9 +14,12 @@ using System.Xml.Linq; namespace Barotrauma.Networking { - partial class GameServer : NetworkMember + sealed class GameServer : NetworkMember { public override bool IsServer => true; + public override bool IsClient => false; + + public override Voting Voting { get; } private string serverName; public string ServerName @@ -25,7 +28,8 @@ namespace Barotrauma.Networking set { if (string.IsNullOrEmpty(value)) { return; } - serverName = value.Replace(":", "").Replace(";", ""); + + serverName = value; } } @@ -49,7 +53,7 @@ namespace Barotrauma.Networking public ServerPeer ServerPeer { get { return serverPeer; } } private DateTime refreshMasterTimer; - private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); + private readonly TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); private bool registeredToMaster; private DateTime roundStartTime; @@ -57,6 +61,11 @@ namespace Barotrauma.Networking private bool autoRestartTimerRunning; private float endRoundTimer; + /// + /// Chat messages that get sent to the owner of the server when the owner is determined + /// + private static readonly Queue pendingMessagesToOwner = new Queue(); + public VoipServer VoipServer { get; @@ -98,15 +107,10 @@ namespace Barotrauma.Networking get { return entityEventManager; } } - public TimeSpan UpdateInterval - { - get { return updateInterval; } - } - - public int Port => serverSettings?.Port ?? 0; + public int Port => ServerSettings?.Port ?? 0; //only used when connected to steam - public int QueryPort => serverSettings?.QueryPort ?? 0; + public int QueryPort => ServerSettings?.QueryPort ?? 0; public NetworkConnection OwnerConnection { get; private set; } private readonly Option ownerKey; @@ -123,8 +127,6 @@ namespace Barotrauma.Networking Option ownerKey, Option ownerSteamId) { - name = name.Replace(":", ""); - name = name.Replace(";", ""); if (name.Length > NetConfig.ServerNameMaxLength) { name = name.Substring(0, NetConfig.ServerNameMaxLength); @@ -134,9 +136,9 @@ namespace Barotrauma.Networking LastClientListUpdateID = 0; - serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); - KarmaManager.SelectPreset(serverSettings.KarmaPreset); - serverSettings.SetPassword(password); + ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); + KarmaManager.SelectPreset(ServerSettings.KarmaPreset); + ServerSettings.SetPassword(password); Voting = new Voting(); @@ -145,91 +147,74 @@ namespace Barotrauma.Networking this.ownerSteamId = ownerSteamId; entityEventManager = new ServerEntityEventManager(this); - - CoroutineManager.StartCoroutine(StartServer(isPublic)); } - private IEnumerable StartServer(bool isPublic) + public void StartServer() { - bool error = false; - try + Log("Starting the server...", ServerLog.MessageType.ServerMessage); + + var callbacks = new ServerPeer.Callbacks( + ReadDataMessage, + OnClientDisconnect, + OnInitializationComplete, + GameMain.Instance.CloseServer, + OnOwnerDetermined); + + if (ownerSteamId.TryUnwrap(out var steamId)) { - Log("Starting the server...", ServerLog.MessageType.ServerMessage); - if (ownerSteamId.TryUnwrap(out var steamId)) - { - Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(steamId, ownerKey.Fallback(0), serverSettings); - } - else - { - 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; - serverPeer.OnMessageReceived = ReadDataMessage; - serverPeer.OnDisconnect = OnClientDisconnect; - serverPeer.OnShutdown = GameMain.Instance.CloseServer; - serverPeer.OnOwnerDetermined = OnOwnerDetermined; - - FileSender = new FileSender(serverPeer, MsgConstants.MTU); - FileSender.OnEnded += FileTransferChanged; - FileSender.OnStarted += FileTransferChanged; - - if (serverSettings.AllowModDownloads) { ModSender = new ModSender(); } - - serverPeer.Start(); - - VoipServer = new VoipServer(serverPeer); + Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); + serverPeer = new SteamP2PServerPeer(steamId, ownerKey.Fallback(0), ServerSettings, callbacks); } - catch (Exception e) + else { - Log("Error while starting the server (" + e.Message + ")", ServerLog.MessageType.Error); - - System.Net.Sockets.SocketException socketException = e as System.Net.Sockets.SocketException; - - error = true; + Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); + serverPeer = new LidgrenServerPeer(ownerKey, ServerSettings, callbacks); } - if (error) - { - if (serverPeer != null) serverPeer.Close("Error while starting the server"); + FileSender = new FileSender(serverPeer, MsgConstants.MTU); + FileSender.OnEnded += FileTransferChanged; + FileSender.OnStarted += FileTransferChanged; - Environment.Exit(-1); + if (ServerSettings.AllowModDownloads) { ModSender = new ModSender(); } - yield return CoroutineStatus.Success; - } + serverPeer.Start(); + VoipServer = new VoipServer(serverPeer); if (serverPeer is LidgrenServerPeer) { #if USE_STEAM - registeredToMaster = SteamManager.CreateServer(this, isPublic); + registeredToMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); #endif } - TickRate = serverSettings.TickRate; - Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); GameMain.NetLobbyScreen.RandomizeSettings(); - if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) + if (!string.IsNullOrEmpty(ServerSettings.SelectedSubmarine)) { - SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == ServerSettings.SelectedSubmarine); if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; } } - if (!string.IsNullOrEmpty(serverSettings.SelectedShuttle)) + if (!string.IsNullOrEmpty(ServerSettings.SelectedShuttle)) { - SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); + SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == ServerSettings.SelectedShuttle); if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } started = true; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); + } - yield return CoroutineStatus.Success; + + /// + /// Creates a message that gets sent to the server owner once the connection is initialized. Can be used to for example notify the owner of problems during initialization + /// + public static void AddPendingMessageToOwner(string message, ChatMessageType messageType) + { + pendingMessagesToOwner.Enqueue(ChatMessage.Create(string.Empty, message, messageType, sender: null)); } private void OnOwnerDetermined(NetworkConnection connection) @@ -251,12 +236,12 @@ namespace Barotrauma.Networking var tempList = ConnectedClients.Where(c => c.Connection != OwnerConnection).ToList(); foreach (var c in tempList) { - DisconnectClient(c.Connection, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + DisconnectClient(c.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); } if (OwnerConnection != null) { var conn = OwnerConnection; OwnerConnection = null; - DisconnectClient(conn, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + DisconnectClient(conn, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); } Thread.Sleep(500); } @@ -296,7 +281,7 @@ namespace Barotrauma.Networking } SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); - serverSettings.ServerDetailsChanged = true; + ServerSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { @@ -305,7 +290,7 @@ namespace Barotrauma.Networking previousPlayer.Name = newClient.Name; } - var savedPermissions = serverSettings.ClientPermissions.Find(scp => + var savedPermissions = ServerSettings.ClientPermissions.Find(scp => scp.AddressOrAccountId.TryGet(out AccountId accountId) ? newClient.AccountId.ValueEquals(accountId) : newClient.Connection.Endpoint.Address == scp.AddressOrAccountId); @@ -336,14 +321,14 @@ namespace Barotrauma.Networking } } - private void OnClientDisconnect(NetworkConnection connection, string disconnectMsg) + private void OnClientDisconnect(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket) { Client connectedClient = connectedClients.Find(c => c.Connection == connection); - DisconnectClient(connectedClient, reason: disconnectMsg); + DisconnectClient(connectedClient, peerDisconnectPacket); } - public override void Update(float deltaTime) + public void Update(float deltaTime) { #if CLIENT if (ShowNetStats) { netStats.Update(deltaTime); } @@ -356,21 +341,19 @@ namespace Barotrauma.Networking return; } - base.Update(deltaTime); - FileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); UpdatePing(); - if (serverSettings.VoiceChatEnabled) + if (ServerSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); } - if (gameStarted) + if (GameStarted) { - respawnManager?.Update(deltaTime); + RespawnManager?.Update(deltaTime); entityEventManager.Update(connectedClients); @@ -385,14 +368,14 @@ namespace Barotrauma.Networking Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); - if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) + if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); continue; } if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!serverSettings.AllowSpectating || !owner.SpectateOnly)) + (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) { SetClientCharacter(owner, character); } @@ -438,7 +421,7 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (serverSettings.AutoRestart && isCrewDead) + else if (ServerSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -448,7 +431,7 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (isCrewDead && respawnManager == null) + else if (isCrewDead && RespawnManager == null) { #if !DEBUG if (endRoundTimer <= 0.0f) @@ -477,7 +460,7 @@ namespace Barotrauma.Networking { Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); } - else if (serverSettings.AutoRestart && isCrewDead) + else if (ServerSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } @@ -485,7 +468,7 @@ namespace Barotrauma.Networking { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } - else if (respawnManager == null) + else if (RespawnManager == null) { Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); } @@ -503,7 +486,7 @@ namespace Barotrauma.Networking // -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine)) { - if (serverSettings.AutoRestart) serverSettings.AutoRestartTimer = Math.Max(serverSettings.AutoRestartInterval, 5.0f); + if (ServerSettings.AutoRestart) ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f); //GameMain.NetLobbyScreen.StartButtonEnabled = true; GameMain.NetLobbyScreen.LastUpdateID++; @@ -512,15 +495,15 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame && + else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) { - if (serverSettings.AutoRestart) + if (ServerSettings.AutoRestart) { //autorestart if there are any non-spectators on the server (ignoring the server owner) bool shouldAutoRestart = connectedClients.Any(c => c.Connection != OwnerConnection && - (!c.SpectateOnly || !serverSettings.AllowSpectating)); + (!c.SpectateOnly || !ServerSettings.AllowSpectating)); if (shouldAutoRestart != autoRestartTimerRunning) { @@ -530,18 +513,18 @@ namespace Barotrauma.Networking if (autoRestartTimerRunning) { - serverSettings.AutoRestartTimer -= deltaTime; + ServerSettings.AutoRestartTimer -= deltaTime; } } - if (serverSettings.AutoRestart && autoRestartTimerRunning && serverSettings.AutoRestartTimer < 0.0f) + if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) { StartGame(); } - else if (serverSettings.StartWhenClientsReady) + else if (ServerSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); - if (clientsReady / (float)connectedClients.Count >= serverSettings.StartWhenClientsReadyRatio) + if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) { StartGame(); } @@ -553,7 +536,7 @@ namespace Barotrauma.Networking disconnectedClients[i].DeleteDisconnectedTimer -= deltaTime; if (disconnectedClients[i].DeleteDisconnectedTimer > 0.0f) continue; - if (gameStarted && disconnectedClients[i].Character != null) + if (GameStarted && disconnectedClients[i].Character != null) { disconnectedClients[i].Character.Kill(CauseOfDeathType.Disconnected, null); disconnectedClients[i].Character = null; @@ -569,16 +552,16 @@ namespace Barotrauma.Networking c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime); //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received) - if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) + if (GameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { if (c.Connection != OwnerConnection && c.Permissions != ClientPermissions.All) { c.KickAFKTimer += deltaTime; } } } - if (connectedClients.Any(c => c.KickAFKTimer >= serverSettings.KickAFKTime)) + if (connectedClients.Any(c => c.KickAFKTimer >= ServerSettings.KickAFKTime)) { IEnumerable kickAFK = connectedClients.FindAll(c => - c.KickAFKTimer >= serverSettings.KickAFKTime && + c.KickAFKTimer >= ServerSettings.KickAFKTime && (OwnerConnection == null || c.Connection != OwnerConnection)); foreach (Client c in kickAFK) { @@ -638,10 +621,10 @@ namespace Barotrauma.Networking } } - updateTimer = DateTime.Now + updateInterval; + updateTimer = DateTime.Now + UpdateInterval; } - if (registeredToMaster && (DateTime.Now > refreshMasterTimer || serverSettings.ServerDetailsChanged)) + if (registeredToMaster && (DateTime.Now > refreshMasterTimer || ServerSettings.ServerDetailsChanged)) { if (GameSettings.CurrentConfig.UseSteamMatchmaking) { @@ -654,7 +637,7 @@ namespace Barotrauma.Networking } } refreshMasterTimer = DateTime.Now + refreshMasterInterval; - serverSettings.ServerDetailsChanged = false; + ServerSettings.ServerDetailsChanged = false; } } @@ -717,7 +700,7 @@ namespace Barotrauma.Networking UpdateCharacterInfo(inc, connectedClient); //game already started -> send start message immediately - if (gameStarted) + if (GameStarted) { SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient, true); } @@ -728,7 +711,7 @@ namespace Barotrauma.Networking { DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message."); } - else if (!gameStarted) + else if (!GameStarted) { DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message."); } @@ -741,7 +724,7 @@ namespace Barotrauma.Networking ClientReadLobby(inc); break; case ClientPacketHeader.UPDATE_INGAME: - if (!gameStarted) { return; } + if (!GameStarted) { return; } ClientReadIngame(inc); break; case ClientPacketHeader.CAMPAIGN_SETUP_INFO: @@ -756,7 +739,7 @@ namespace Barotrauma.Networking var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); - if (gameStarted) + if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; @@ -782,7 +765,7 @@ namespace Barotrauma.Networking else { string saveName = inc.ReadString(); - if (gameStarted) + if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; @@ -791,7 +774,7 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.VOICE: - if (serverSettings.VoiceChatEnabled && !connectedClient.Muted) + if (ServerSettings.VoiceChatEnabled && !connectedClient.Muted) { byte id = inc.ReadByte(); if (connectedClient.SessionId != id) @@ -806,7 +789,7 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.SERVER_SETTINGS: - serverSettings.ServerRead(inc, connectedClient); + ServerSettings.ServerRead(inc, connectedClient); break; case ClientPacketHeader.SERVER_COMMAND: ClientReadServerCommand(inc); @@ -830,7 +813,7 @@ namespace Barotrauma.Networking ReadReadyToSpawnMessage(inc, connectedClient); break; case ClientPacketHeader.FILE_REQUEST: - if (serverSettings.AllowFileTransfers) + if (ServerSettings.AllowFileTransfers) { FileSender.ReadFileRequest(inc, connectedClient); } @@ -887,7 +870,7 @@ namespace Barotrauma.Networking { errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } - if (gameStarted) + if (GameStarted) { var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) @@ -1107,7 +1090,7 @@ namespace Barotrauma.Networking bool midroundSyncingDone = inc.ReadBoolean(); inc.ReadPadBits(); - if (gameStarted) + if (GameStarted) { if (!c.InGame) { @@ -1170,7 +1153,7 @@ namespace Barotrauma.Networking c.LastRecvEntityEventID = lastRecvEntityEventID; DebugConsole.Log("Finished midround syncing " + c.Name + " - switching from ID " + prevID + " to " + c.LastRecvEntityEventID); //notify the client of the state of the respawn manager (so they show the respawn prompt if needed) - if (respawnManager != null) { CreateEntityEvent(respawnManager); } + if (RespawnManager != null) { CreateEntityEvent(RespawnManager); } if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { //notify the client of the current bank balance and purchased repairs @@ -1291,7 +1274,7 @@ namespace Barotrauma.Networking private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { - sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); sender.WaitForNextRoundRespawn = inc.ReadBoolean(); if (!(GameMain.GameSession?.GameMode is CampaignMode)) { @@ -1399,7 +1382,7 @@ namespace Barotrauma.Networking mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) { bool save = inc.ReadBoolean(); - if (gameStarted) + if (GameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) @@ -1421,7 +1404,7 @@ namespace Barotrauma.Networking bool continueCampaign = inc.ReadBoolean(); if (mpCampaign != null && mpCampaign.GameOver || continueCampaign) { - if (gameStarted) + if (GameStarted) { SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error); break; @@ -1432,7 +1415,7 @@ namespace Barotrauma.Networking } } - else if (!gameStarted && !initiatedStartGame) + else if (!GameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); @@ -1562,7 +1545,7 @@ namespace Barotrauma.Networking private void ClientWrite(Client c) { - if (gameStarted && c.InGame) + if (GameStarted && c.InGame) { ClientWriteIngame(c); } @@ -1570,7 +1553,7 @@ namespace Barotrauma.Networking { //if 30 seconds have passed since the round started and the client isn't ingame yet, //consider the client's character disconnected (causing it to die if the client does not join soon) - if (gameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f) + if (GameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f) { c.Character.ClientDisconnected = true; } @@ -1578,6 +1561,15 @@ namespace Barotrauma.Networking ClientWriteLobby(c); } + + if (c.Connection == OwnerConnection) + { + while (pendingMessagesToOwner.Any()) + { + SendDirectChatMessage(pendingMessagesToOwner.Dequeue(), c); + } + } + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && GameMain.NetLobbyScreen.SelectedMode == campaign.Preset && NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) @@ -1625,7 +1617,7 @@ namespace Barotrauma.Networking } outmsg.WriteBoolean(GameStarted); - outmsg.WriteBoolean(serverSettings.AllowSpectating); + outmsg.WriteBoolean(ServerSettings.AllowSpectating); c.WritePermissions(outmsg); } @@ -1834,11 +1826,11 @@ namespace Barotrauma.Networking AccountInfo = client.AccountInfo, NameId = client.NameId, Name = client.Name, - PreferredJob = client.Character?.Info?.Job != null && gameStarted + PreferredJob = client.Character?.Info?.Job != null && GameStarted ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob, PreferredTeam = client.PreferredTeam, - CharacterId = client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID, + CharacterId = client.Character == null || !GameStarted ? (ushort)0 : client.Character.ID, Karma = c.HasPermission(ClientPermissions.ServerLog) ? client.Karma : 100.0f, Muted = client.Muted, InGame = client.InGame, @@ -1882,7 +1874,7 @@ namespace Barotrauma.Networking outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); settingsBuf = new ReadWriteMessage(); - serverSettings.ServerWrite(settingsBuf, c); + ServerSettings.ServerWrite(settingsBuf, c); outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); @@ -1897,34 +1889,34 @@ namespace Barotrauma.Networking outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); outmsg.WriteBoolean(IsUsingRespawnShuttle()); - var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? - respawnManager.RespawnShuttle.Info : + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; outmsg.WriteString(selectedShuttle.Name); outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); - outmsg.WriteBoolean(serverSettings.AllowSubVoting); - outmsg.WriteBoolean(serverSettings.AllowModeVoting); + outmsg.WriteBoolean(ServerSettings.AllowSubVoting); + outmsg.WriteBoolean(ServerSettings.AllowModeVoting); - outmsg.WriteBoolean(serverSettings.VoiceChatEnabled); + outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); - outmsg.WriteBoolean(serverSettings.AllowSpectating); + outmsg.WriteBoolean(ServerSettings.AllowSpectating); - outmsg.WriteRangedInteger((int)serverSettings.TraitorsEnabled, 0, 2); + outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); - outmsg.WriteSingle(serverSettings.SelectedLevelDifficulty); + outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); - outmsg.WriteByte((byte)serverSettings.BotCount); - outmsg.WriteBoolean(serverSettings.BotSpawnMode == BotSpawnMode.Fill); + outmsg.WriteByte((byte)ServerSettings.BotCount); + outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); - outmsg.WriteBoolean(serverSettings.AutoRestart); - if (serverSettings.AutoRestart) + outmsg.WriteBoolean(ServerSettings.AutoRestart); + if (ServerSettings.AutoRestart) { - outmsg.WriteSingle(autoRestartTimerRunning ? serverSettings.AutoRestartTimer : 0.0f); + outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); } } else @@ -2032,13 +2024,13 @@ namespace Barotrauma.Networking public bool StartGame() { - if (initiatedStartGame || gameStarted) { return false; } + if (initiatedStartGame || GameStarted) { return false; } Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; SubmarineInfo selectedSub; - if (serverSettings.AllowSubVoting) + if (ServerSettings.AllowSubVoting) { selectedSub = Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } @@ -2163,7 +2155,7 @@ namespace Barotrauma.Networking List playingClients = new List(connectedClients); - if (serverSettings.AllowSpectating) + if (ServerSettings.AllowSpectating) { playingClients.RemoveAll(c => c.SpectateOnly); } @@ -2209,7 +2201,7 @@ namespace Barotrauma.Networking else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); + GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, ServerSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); @@ -2231,9 +2223,9 @@ namespace Barotrauma.Networking bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - if (serverSettings.AllowRespawn && missionAllowRespawn) + if (ServerSettings.AllowRespawn && missionAllowRespawn) { - respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); + RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); } if (campaign != null) { @@ -2273,7 +2265,7 @@ namespace Barotrauma.Networking //find the clients in this team List teamClients = teamCount == 1 ? new List(playingClients) : playingClients.FindAll(c => c.TeamID == teamID); - if (serverSettings.AllowSpectating) + if (ServerSettings.AllowSpectating) { teamClients.RemoveAll(c => c.SpectateOnly); } @@ -2311,7 +2303,7 @@ namespace Barotrauma.Networking // do not load new bots if we already have them if (crewManager == null || !crewManager.HasBots) { - int botsToSpawn = serverSettings.BotSpawnMode == BotSpawnMode.Fill ? serverSettings.BotCount - characterInfos.Count : serverSettings.BotCount; + int botsToSpawn = ServerSettings.BotSpawnMode == BotSpawnMode.Fill ? ServerSettings.BotCount - characterInfos.Count : ServerSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) { var botInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName) @@ -2449,7 +2441,7 @@ namespace Barotrauma.Networking { if (sub == null) { continue; } List spawnList = new List(); - foreach (KeyValuePair kvp in serverSettings.ExtraCargo) + foreach (KeyValuePair kvp in ServerSettings.ExtraCargo) { spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } @@ -2458,8 +2450,8 @@ namespace Barotrauma.Networking } TraitorManager = null; - if (serverSettings.TraitorsEnabled == YesNoMaybe.Yes || - (serverSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) + if (ServerSettings.TraitorsEnabled == YesNoMaybe.Yes || + (ServerSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) { if (!(GameMain.GameSession?.GameMode is CampaignMode)) { @@ -2472,13 +2464,13 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - Voting?.ResetVotes(GameMain.Server.ConnectedClients); + Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); - gameStarted = true; + GameStarted = true; initiatedStartGame = false; GameMain.ResetFrameTime(); @@ -2508,28 +2500,28 @@ namespace Barotrauma.Networking msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); - msg.WriteBoolean(serverSettings.AllowRespawn && missionAllowRespawn); - msg.WriteBoolean(serverSettings.AllowDisguises); - msg.WriteBoolean(serverSettings.AllowRewiring); - msg.WriteBoolean(serverSettings.AllowFriendlyFire); - msg.WriteBoolean(serverSettings.LockAllDefaultWires); - msg.WriteBoolean(serverSettings.AllowRagdollButton); - msg.WriteBoolean(serverSettings.AllowLinkingWifiToChat); - msg.WriteInt32(serverSettings.MaximumMoneyTransferRequest); + msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); + msg.WriteBoolean(ServerSettings.AllowDisguises); + msg.WriteBoolean(ServerSettings.AllowRewiring); + msg.WriteBoolean(ServerSettings.AllowFriendlyFire); + msg.WriteBoolean(ServerSettings.LockAllDefaultWires); + msg.WriteBoolean(ServerSettings.AllowRagdollButton); + msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); + msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); - msg.WriteByte((byte)serverSettings.LosMode); + msg.WriteByte((byte)ServerSettings.LosMode); msg.WriteBoolean(includesFinalize); msg.WritePadBits(); - serverSettings.WriteMonsterEnabled(msg); + ServerSettings.WriteMonsterEnabled(msg); if (campaign == null) { msg.WriteString(levelSeed); - msg.WriteSingle(serverSettings.SelectedLevelDifficulty); + msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); msg.WriteString(gameSession.SubmarineInfo.Name); msg.WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); - var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? - respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.WriteString(selectedShuttle.Name); msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation); msg.WriteByte((byte)GameMain.GameSession.GameMode.Missions.Count()); @@ -2560,7 +2552,7 @@ namespace Barotrauma.Networking private bool IsUsingRespawnShuttle() { - return serverSettings.UseRespawnShuttle || (gameStarted && respawnManager != null && respawnManager.UsingShuttle); + return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); } private void SendRoundStartFinalize(Client client) @@ -2600,7 +2592,7 @@ namespace Barotrauma.Networking public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false) { - if (!gameStarted) + if (!GameStarted) { return; } @@ -2626,14 +2618,14 @@ namespace Barotrauma.Networking endRoundTimer = 0.0f; - if (serverSettings.AutoRestart) + if (ServerSettings.AutoRestart) { - serverSettings.AutoRestartTimer = serverSettings.AutoRestartInterval; + ServerSettings.AutoRestartTimer = ServerSettings.AutoRestartInterval; //send a netlobby update to get the clients' autorestart timers up to date GameMain.NetLobbyScreen.LastUpdateID++; } - if (serverSettings.SaveServerLogs) { serverSettings.ServerLog.Save(); } + if (ServerSettings.SaveServerLogs) { ServerSettings.ServerLog.Save(); } GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; @@ -2647,12 +2639,8 @@ namespace Barotrauma.Networking KarmaManager.OnRoundEnded(); -#if DEBUG - messageCount.Clear(); -#endif - - respawnManager = null; - gameStarted = false; + RespawnManager = null; + GameStarted = false; if (connectedClients.Count > 0) { @@ -2762,7 +2750,7 @@ namespace Barotrauma.Networking if (c.Connection != OwnerConnection) { - if (!Client.IsValidName(newName, serverSettings)) + if (!Client.IsValidName(newName, ServerSettings)) { SendDirectChatMessage($"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; @@ -2809,7 +2797,7 @@ namespace Barotrauma.Networking public void KickClient(Client client, string reason, bool resetKarma = false) { - if (client == null || client.Connection == OwnerConnection) return; + if (client == null || client.Connection == OwnerConnection) { return; } if (resetKarma) { @@ -2821,9 +2809,7 @@ namespace Barotrauma.Networking client.Karma = Math.Max(client.Karma, 50.0f); } - string msg = DisconnectReason.Kicked.ToString(); - string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; - DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); + DisconnectClient(client, PeerDisconnectPacket.Kicked(reason)); } public override void BanPlayer(string playerName, string reason, TimeSpan? duration = null) @@ -2853,17 +2839,19 @@ namespace Barotrauma.Networking } client.Karma = Math.Max(client.Karma, 50.0f); - string targetMsg = DisconnectReason.Banned.ToString(); - DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); + DisconnectClient(client, PeerDisconnectPacket.Banned(reason)); - serverSettings.BanList.BanPlayer(client.Name, client.Connection.Endpoint, reason, duration); if (client.AccountInfo.AccountId.TryUnwrap(out var accountId)) { - serverSettings.BanList.BanPlayer(client.Name, accountId, reason, duration); + ServerSettings.BanList.BanPlayer(client.Name, accountId, reason, duration); + } + else + { + ServerSettings.BanList.BanPlayer(client.Name, client.Connection.Endpoint, reason, duration); } foreach (var relatedId in client.AccountInfo.OtherMatchingIds) { - serverSettings.BanList.BanPlayer(client.Name, relatedId, reason, duration); + ServerSettings.BanList.BanPlayer(client.Name, relatedId, reason, duration); } } @@ -2874,14 +2862,14 @@ namespace Barotrauma.Networking //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); - serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.Address, reason, duration); + ServerSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.Address, reason, duration); if (previousPlayer.AccountInfo.AccountId.TryUnwrap(out var accountId)) { - serverSettings.BanList.BanPlayer(previousPlayer.Name, accountId, reason, duration); + ServerSettings.BanList.BanPlayer(previousPlayer.Name, accountId, reason, duration); } foreach (var relatedId in previousPlayer.AccountInfo.OtherMatchingIds) { - serverSettings.BanList.BanPlayer(previousPlayer.Name, relatedId, reason, duration); + ServerSettings.BanList.BanPlayer(previousPlayer.Name, relatedId, reason, duration); } string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}"; @@ -2895,31 +2883,25 @@ namespace Barotrauma.Networking public override void UnbanPlayer(string playerName) { BannedPlayer bannedPlayer - = serverSettings.BanList.BannedPlayers.FirstOrDefault(bp => bp.Name == playerName); + = ServerSettings.BanList.BannedPlayers.FirstOrDefault(bp => bp.Name == playerName); if (bannedPlayer is null) { return; } - serverSettings.BanList.UnbanPlayer(bannedPlayer.AddressOrAccountId); + ServerSettings.BanList.UnbanPlayer(bannedPlayer.AddressOrAccountId); } public override void UnbanPlayer(Endpoint endpoint) { - serverSettings.BanList.UnbanPlayer(endpoint); + ServerSettings.BanList.UnbanPlayer(endpoint); } - public void DisconnectClient(NetworkConnection senderConnection, string msg = "", string targetmsg = "") + public void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket) { - if (senderConnection == OwnerConnection) - { - DebugConsole.NewMessage("Owner disconnected: closing the server...", Color.Yellow); - Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); - GameMain.ShouldRun = false; - } Client client = connectedClients.Find(x => x.Connection == senderConnection); - if (client == null) return; + if (client == null) { return; } - DisconnectClient(client, msg, targetmsg, string.Empty, PlayerConnectionChangeType.Disconnected); + DisconnectClient(client, peerDisconnectPacket); } - public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "", PlayerConnectionChangeType changeType = PlayerConnectionChangeType.Disconnected) + public void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectPacket) { if (client == null) return; @@ -2934,14 +2916,6 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={ClientLogName(client)}"; } - if (string.IsNullOrWhiteSpace(targetmsg)) { targetmsg = "ServerMessage.YouLeftServer"; } - if (!string.IsNullOrWhiteSpace(reason)) - { - msg += $"/ /ServerMessage.Reason/: /{reason}"; - targetmsg += $"/\n/ServerMessage.Reason/: /{reason}"; - } - if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); @@ -2959,19 +2933,19 @@ namespace Barotrauma.Networking if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); } } - serverPeer.Disconnect(client.Connection, targetmsg); client.Dispose(); connectedClients.Remove(client); + serverPeer.Disconnect(client.Connection, peerDisconnectPacket); KarmaManager.OnClientDisconnected(client); UpdateVoteStatus(); - SendChatMessage(msg, ChatMessageType.Server, changeType: changeType); + SendChatMessage(peerDisconnectPacket.ChatMessage(client).Value, ChatMessageType.Server, changeType: peerDisconnectPacket.ConnectionChangeType); UpdateCrewFrame(); - serverSettings.ServerDetailsChanged = true; + ServerSettings.ServerDetailsChanged = true; refreshMasterTimer = DateTime.Now; } @@ -3086,7 +3060,7 @@ namespace Barotrauma.Networking message = tempStr; } - if (gameStarted) + if (GameStarted) { if (senderClient == null) { @@ -3282,11 +3256,11 @@ namespace Barotrauma.Networking int no = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 1); int max = eligibleClients.Count(); // Required ratio cannot be met - if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) + if (no / (float)max > 1f - ServerSettings.VoteRequiredRatio) { Voting.ActiveVote.Finish(Voting, passed: false); } - else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + else if (yes / (float)max >= ServerSettings.VoteRequiredRatio) { Voting.ActiveVote.Finish(Voting, passed: true); } @@ -3296,7 +3270,7 @@ namespace Barotrauma.Networking Client.UpdateKickVotes(connectedClients); var kickVoteEligibleClients = connectedClients.Where(c => (DateTime.Now - c.JoinTime).TotalSeconds > ServerSettings.DisallowKickVoteTime); - float minimumKickVotes = Math.Max(2.0f, kickVoteEligibleClients.Count() * serverSettings.KickVoteRequiredRatio); + float minimumKickVotes = Math.Max(2.0f, kickVoteEligibleClients.Count() * ServerSettings.KickVoteRequiredRatio); var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && !c.HasPermission(ClientPermissions.Kick) && @@ -3306,13 +3280,9 @@ namespace Barotrauma.Networking foreach (Client c in clientsToKick) { //reset the client's kick votes (they can rejoin after their ban expires) - c.ResetVotes(); - var previousPlayer = previousPlayers.Find(p => p.MatchesClient(c)); - previousPlayer?.KickVoters.Clear(); - - SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Kicked); - KickClient(c, "ServerMessage.KickedByVote"); - BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); + c.ResetVotes(resetKickVotes: true); + previousPlayers.Where(p => p.MatchesClient(c)).ForEach(p => p.KickVoters.Clear()); + BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(ServerSettings.AutoBanTime)); } //GameMain.NetLobbyScreen.LastUpdateID++; @@ -3321,8 +3291,8 @@ namespace Barotrauma.Networking int endVoteCount = ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); int endVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned); - if (serverSettings.AllowEndVoting && endVoteMax > 0 && - ((float)endVoteCount / (float)endVoteMax) >= serverSettings.EndVoteRequiredRatio) + if (ServerSettings.AllowEndVoting && endVoteMax > 0 && + ((float)endVoteCount / (float)endVoteMax) >= ServerSettings.EndVoteRequiredRatio) { Log("Ending round by votes (" + endVoteCount + "/" + (endVoteMax - endVoteCount) + ")", ServerLog.MessageType.ServerMessage); EndGame(wasSaved: false); @@ -3380,10 +3350,10 @@ namespace Barotrauma.Networking { if (client.AccountId.TryUnwrap(out var accountId)) { - serverSettings.ClientPermissions.RemoveAll(scp => scp.AddressOrAccountId == accountId); + ServerSettings.ClientPermissions.RemoveAll(scp => scp.AddressOrAccountId == accountId); if (client.Permissions != ClientPermissions.None) { - serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + ServerSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, accountId, client.Permissions, @@ -3392,10 +3362,10 @@ namespace Barotrauma.Networking } else { - serverSettings.ClientPermissions.RemoveAll(scp => client.Connection.Endpoint.Address == scp.AddressOrAccountId); + ServerSettings.ClientPermissions.RemoveAll(scp => client.Connection.Endpoint.Address == scp.AddressOrAccountId); if (client.Permissions != ClientPermissions.None) { - serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + ServerSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, client.Connection.Endpoint.Address, client.Permissions, @@ -3407,7 +3377,7 @@ namespace Barotrauma.Networking { CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(recipient, client)); } - serverSettings.SaveClientPermissions(); + ServerSettings.SaveClientPermissions(); } private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) @@ -3557,7 +3527,7 @@ namespace Barotrauma.Networking private void UpdateCharacterInfo(IReadMessage message, Client sender) { - sender.SpectateOnly = message.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); if (sender.SpectateOnly) { return; @@ -3935,29 +3905,29 @@ namespace Barotrauma.Networking } } - public override void Quit() + public void Quit() { if (started) { started = false; - serverSettings.BanList.Save(); + ServerSettings.BanList.Save(); - if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } - if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } + if (GameMain.NetLobbyScreen.SelectedSub != null) { ServerSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } + if (GameMain.NetLobbyScreen.SelectedShuttle != null) { ServerSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } - serverSettings.SaveSettings(); + ServerSettings.SaveSettings(); ModSender.Dispose(); - if (serverSettings.SaveServerLogs) + if (ServerSettings.SaveServerLogs) { Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); - serverSettings.ServerLog.Save(); + ServerSettings.ServerLog.Save(); } GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown"); - serverPeer?.Close(DisconnectReason.ServerShutdown.ToString()); + serverPeer?.Close(); SteamManager.CloseServer(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 0bc4f9d87..abcbbf42c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Networking " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error); - server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncOldEvent + "/ServerMessage.ExcessiveDesyncOldEvent"); + server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent)); } ); } @@ -260,7 +260,7 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red); GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); - server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent"); + server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncRemovedEvent)); }); } } @@ -269,7 +269,7 @@ namespace Barotrauma.Networking foreach (Client timedOutClient in timedOutClients) { GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); - GameMain.Server.DisconnectClient(timedOutClient, "", DisconnectReason.SyncTimeout + "/ServerMessage.SyncTimeout"); + GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout)); } bufferedEvents.RemoveAll(b => b.IsProcessed); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index dbf398cae..21d3f9ae8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Networking if (Sender != null && c.InGame) { msg.WriteUInt16(Sender.ID); - } + } msg.WriteBoolean(false); //text color (no custom text colors for order messages) msg.WritePadBits(); WriteOrder(msg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 51e8c3749..c49417f23 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Networking private readonly List incomingLidgrenMessages; - public LidgrenServerPeer(Option ownKey, ServerSettings settings) + public LidgrenServerPeer(Option ownKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) { serverSettings = settings; @@ -68,21 +68,21 @@ namespace Barotrauma.Networking } } - public override void Close(string? msg = null) + public override void Close() { if (netServer == null) { return; } for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); + RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } - netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString()); + netServer.Shutdown(PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown).ToLidgrenStringRepresentation()); pendingClients.Clear(); connectedClients.Clear(); @@ -91,7 +91,7 @@ namespace Barotrauma.Networking Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange; - OnShutdown?.Invoke(); + callbacks.OnShutdown.Invoke(); } public override void Update(float deltaTime) @@ -100,12 +100,6 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(incomingLidgrenMessages); - if (OnOwnerDetermined != null && OwnerConnection != null) - { - OnOwnerDetermined?.Invoke(OwnerConnection); - OnOwnerDetermined = null; - } - netServer.ReadMessages(incomingLidgrenMessages); //process incoming connections first @@ -193,14 +187,14 @@ namespace Barotrauma.Networking if (connectedClients.Count >= serverSettings.MaxPlayers) { - inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString()); + inc.SenderConnection.Deny(PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull).ToLidgrenStringRepresentation()); return; } if (serverSettings.BanList.IsBanned(new LidgrenEndpoint(inc.SenderConnection.RemoteEndPoint), out string banReason)) { //IP banned: deny immediately - inc.SenderConnection.Deny($"{DisconnectReason.Banned}/ {banReason}"); + inc.SenderConnection.Deny(PeerDisconnectPacket.Banned(banReason).ToLidgrenStringRepresentation()); return; } @@ -235,12 +229,12 @@ namespace Barotrauma.Networking { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired, "Received data message from unauthenticated client"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired)); } else if (lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnected && lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnecting) { - lidgrenMsg.SenderConnection.Disconnect($"{DisconnectReason.AuthenticationRequired}/ Received data message from unauthenticated client"); + lidgrenMsg.SenderConnection.Disconnect(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired).ToLidgrenStringRepresentation()); } return; @@ -252,12 +246,12 @@ namespace Barotrauma.Networking || (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}/ {banReason}"); + Disconnect(conn, PeerDisconnectPacket.Banned(banReason)); return; } var packet = INetSerializableStruct.Read(inc); - OnMessageReceived?.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); + callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); } } @@ -275,12 +269,11 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage("Owner disconnected: closing the server..."); GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); - Close($"{DisconnectReason.ServerShutdown}/ Owner disconnected"); + Close(); } else { -#warning TODO: kill off disconnect in layer 1 - Disconnect(conn, $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == conn).Name}"); + Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } else @@ -288,7 +281,7 @@ namespace Barotrauma.Networking PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } @@ -312,9 +305,11 @@ namespace Barotrauma.Networking { if (status == Steamworks.AuthResponse.OK) { return; } - if (connectedClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) is LidgrenConnection connection) + if (connectedClients.Find(c + => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + is LidgrenConnection connection) { - Disconnect(connection, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication status changed: {status}"); + Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); } return; @@ -325,7 +320,7 @@ namespace Barotrauma.Networking || serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason) || serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); return; } @@ -337,7 +332,7 @@ namespace Barotrauma.Networking } else { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, $"Steam authentication failed: {status}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(status)); } } @@ -374,7 +369,7 @@ namespace Barotrauma.Networking SendMsgInternal(conn, headers, body); } - public override void Disconnect(NetworkConnection conn, string? msg = null) + public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) { if (netServer == null) { return; } @@ -384,11 +379,11 @@ namespace Barotrauma.Networking { lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); - OnDisconnect?.Invoke(conn, msg); + callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } } - lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected"); + lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); } protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) @@ -413,6 +408,7 @@ namespace Barotrauma.Networking { ownerKey = Option.None(); OwnerConnection = pendingClient.Connection; + callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } } @@ -440,7 +436,7 @@ namespace Barotrauma.Networking { if (requireSteamAuth) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: Steam ID not provided"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); return; } } @@ -451,7 +447,7 @@ namespace Barotrauma.Networking { if (requireSteamAuth) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, $"Steam auth session failed to start: {authSessionStartState}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); } else { @@ -471,7 +467,7 @@ namespace Barotrauma.Networking { if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid)) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index a00aa7a17..bc8acf2d6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -9,26 +9,31 @@ namespace Barotrauma.Networking { internal abstract class ServerPeer { - public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); + public readonly record struct Callbacks( + Callbacks.MessageCallback OnMessageReceived, + Callbacks.DisconnectCallback OnDisconnect, + Callbacks.InitializationCompleteCallback OnInitializationComplete, + Callbacks.ShutdownCallback OnShutdown, + Callbacks.OwnerDeterminedCallback OnOwnerDetermined) + { + public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); + public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket); + public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName); + public delegate void ShutdownCallback(); + public delegate void OwnerDeterminedCallback(NetworkConnection connection); + } - public delegate void DisconnectCallback(NetworkConnection connection, string? reason); - - public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName); - - public delegate void ShutdownCallback(); - - public delegate void OwnerDeterminedCallback(NetworkConnection connection); - - public MessageCallback? OnMessageReceived; - public DisconnectCallback? OnDisconnect; - public InitializationCompleteCallback? OnInitializationComplete; - public ShutdownCallback? OnShutdown; - public OwnerDeterminedCallback? OnOwnerDetermined; + protected readonly Callbacks callbacks; + protected ServerPeer(Callbacks callbacks) + { + this.callbacks = callbacks; + } + public abstract void InitializeSteamServerCallbacks(); public abstract void Start(); - public abstract void Close(string? msg = null); + public abstract void Close(); public abstract void Update(float deltaTime); protected sealed class PendingClient @@ -84,15 +89,16 @@ namespace Barotrauma.Networking if (!Client.IsValidName(authPacket.Name, serverSettings)) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName, ""); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName)); return; } - bool isCompatibleVersion = NetworkMember.IsCompatible(authPacket.GameVersion, GameMain.Version.ToString()) ?? false; + bool isCompatibleVersion = + Version.TryParse(authPacket.GameVersion, out var remoteVersion) + && NetworkMember.IsCompatible(remoteVersion, GameMain.Version); if (!isCompatibleVersion) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={authPacket.GameVersion}"); + RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion()); GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); @@ -104,7 +110,7 @@ namespace Barotrauma.Networking Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower())); if (nameTaken != null) { - RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.NameTaken)); GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); return; } @@ -135,7 +141,7 @@ namespace Barotrauma.Networking { const string banMsg = "Failed to enter correct password too many times"; BanPendingClient(pendingClient, banMsg, null); - RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg)); return; } } @@ -190,13 +196,13 @@ namespace Barotrauma.Networking { if (IsPendingClientBanned(pendingClient, out string? banReason)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); return; } if (connectedClients.Count >= serverSettings.MaxPlayers) { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull)); } if (pendingClient.InitializationStep == ConnectionInitialization.Success) @@ -205,15 +211,15 @@ namespace Barotrauma.Networking connectedClients.Add(newConnection); pendingClients.Remove(pendingClient); - CheckOwnership(pendingClient); + callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name); - OnInitializationComplete?.Invoke(newConnection, pendingClient.Name); + CheckOwnership(pendingClient); } pendingClient.TimeOut -= Timing.Step; if (pendingClient.TimeOut < 0.0) { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } if (Timing.TotalTime < pendingClient.UpdateTime) { return; } @@ -237,7 +243,7 @@ namespace Barotrauma.Networking structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, - ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent) + ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .ToImmutableArray() }; @@ -267,11 +273,11 @@ namespace Barotrauma.Networking protected virtual void CheckOwnership(PendingClient pendingClient) { } - protected void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string? msg) + protected void RemovePendingClient(PendingClient pendingClient, PeerDisconnectPacket peerDisconnectPacket) { if (pendingClients.Contains(pendingClient)) { - Disconnect(pendingClient.Connection, $"{reason}/{msg}"); + Disconnect(pendingClient.Connection, peerDisconnectPacket); pendingClients.Remove(pendingClient); @@ -285,6 +291,6 @@ namespace Barotrauma.Networking } public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); - public abstract void Disconnect(NetworkConnection conn, string? msg = null); + public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 70976272e..8bf68f4b5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -16,7 +17,7 @@ namespace Barotrauma.Networking private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64); - public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings) + public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) { serverSettings = settings; @@ -43,7 +44,7 @@ namespace Barotrauma.Networking started = true; } - public override void Close(string? msg = null) + public override void Close() { if (!started) { return; } @@ -51,12 +52,12 @@ namespace Barotrauma.Networking for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); + RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } pendingClients.Clear(); @@ -64,19 +65,13 @@ namespace Barotrauma.Networking ChildServerRelay.ShutDown(); - OnShutdown?.Invoke(); + callbacks.OnShutdown.Invoke(); } public override void Update(float deltaTime) { if (!started) { return; } - if (OnOwnerDetermined != null && OwnerConnection != null) - { - OnOwnerDetermined?.Invoke(OwnerConnection); - OnOwnerDetermined = null; - } - //backwards for loop so we can remove elements while iterating for (int i = connectedClients.Count - 1; i >= 0; i--) { @@ -84,7 +79,7 @@ namespace Barotrauma.Networking conn.Decay(deltaTime); if (conn.Timeout < 0.0) { - Disconnect(conn, "Timed out"); + Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); } } @@ -149,24 +144,22 @@ namespace Barotrauma.Networking { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason)); } else if (connectedClient != null) { - Disconnect(connectedClient, $"{DisconnectReason.Banned}/ {banReason}"); + Disconnect(connectedClient, PeerDisconnectPacket.Banned(banReason)); } } else if (packetHeader.IsDisconnectMessage()) { if (pendingClient != null) { - string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; - RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg); + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } else if (connectedClient != null) { - string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == connectedClient).Name}"; - Disconnect(connectedClient, disconnectMsg, false); + Disconnect(connectedClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } else if (packetHeader.IsHeartbeatMessage()) @@ -176,28 +169,27 @@ namespace Barotrauma.Networking } else if (packetHeader.IsConnectionInitializationStep()) { + if (!initialization.HasValue) { return; } + ConnectionInitialization initializationStep = initialization.Value; + if (pendingClient != null) { pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); ReadConnectionInitializationStep( pendingClient, new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false), - initialization ?? throw new Exception("Initialization step missing")); + initializationStep); } - else if (initialization.HasValue) + else if (initializationStep == ConnectionInitialization.ConnectionStarted) { - ConnectionInitialization initializationStep = initialization.Value; - if (initializationStep == ConnectionInitialization.ConnectionStarted) - { - pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId))); - } + pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId))); } } else if (connectedClient != null) { var packet = INetSerializableStruct.Read(inc); IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient); - OnMessageReceived?.Invoke(connectedClient, msg); + callbacks.OnMessageReceived.Invoke(connectedClient, msg); } } else //sender is owner @@ -227,7 +219,8 @@ namespace Barotrauma.Networking }; OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId)); - OnInitializationComplete?.Invoke(OwnerConnection, packet.OwnerName); + callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.OwnerName); + callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } return; @@ -241,7 +234,7 @@ namespace Barotrauma.Networking { var packet = INetSerializableStruct.Read(inc); IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, OwnerConnection); - OnMessageReceived?.Invoke(OwnerConnection!, msg); + callbacks.OnMessageReceived.Invoke(OwnerConnection!, msg); } } } @@ -281,27 +274,21 @@ namespace Barotrauma.Networking SendMsgInternal(steamP2PConn, headers, body); } - private void SendDisconnectMessage(SteamId steamId, string? msg) + private void SendDisconnectMessage(SteamId steamId, PeerDisconnectPacket peerDisconnectPacket) { if (!started) { return; } - if (string.IsNullOrWhiteSpace(msg)) { return; } - var headers = new PeerPacketHeaders { DeliveryMethod = DeliveryMethod.Reliable, PacketHeader = PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage, Initialization = null }; - var packet = new PeerDisconnectPacket - { - Message = msg - }; - SendMsgInternal(steamId, headers, packet); + SendMsgInternal(steamId, headers, peerDisconnectPacket); } - private void Disconnect(NetworkConnection conn, string? msg, bool sendDisconnectMessage) + public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) { if (!started) { return; } @@ -309,26 +296,21 @@ namespace Barotrauma.Networking if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || !(connAccountId is SteamId connSteamId)) { return; } - if (sendDisconnectMessage) { SendDisconnectMessage(connSteamId, msg); } + SendDisconnectMessage(connSteamId, peerDisconnectPacket); if (connectedClients.Contains(steamp2pConn)) { steamp2pConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(steamp2pConn); - OnDisconnect?.Invoke(conn, msg); + callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); Steam.SteamManager.StopAuthSession(connSteamId); } else if (steamp2pConn == OwnerConnection) { - //TODO: fix? + throw new InvalidOperationException("Cannot disconnect owner peer"); } } - public override void Disconnect(NetworkConnection conn, string? msg = null) - { - Disconnect(conn, msg, true); - } - protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) { var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 6d0acea10..b8f298682 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -505,9 +505,10 @@ namespace Barotrauma.Networking var characterData = campaign?.GetClientCharacterData(clients[i]); if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) { + //we need to reapply the previous respawn penalty affliction or successive deaths won't make it stack + characterData.ApplyHealthData(character, (AfflictionPrefab ap) => ap == GetRespawnPenaltyAfflictionPrefab()); GiveRespawnPenaltyAffliction(character); } - if (characterData == null || characterData.HasSpawned) { //give the character the items they would've gotten if they had spawned in the main sub @@ -555,7 +556,7 @@ namespace Barotrauma.Networking foreach (Skill skill in characterInfo.Job.GetSkills()) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); - if (skillPrefab == null) { continue; } + if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 3a997b41a..603683348 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -18,10 +18,14 @@ namespace Barotrauma.Networking { if (!PropEquals(lastSyncedValue, Value)) { - LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID); + LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID; lastSyncedValue = Value; } } + public void ForceUpdate() + { + LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID++; + } } public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; @@ -54,6 +58,15 @@ namespace Barotrauma.Networking LoadClientPermissions(); } + public void ForcePropertyUpdate() + { + UpdateFlag(NetFlags.Properties); + foreach (NetPropertyData property in netProperties.Values) + { + property.ForceUpdate(); + } + } + private void WriteNetProperties(IWriteMessage outMsg, Client c) { foreach (UInt32 key in netProperties.Keys) @@ -197,27 +210,32 @@ namespace Barotrauma.Networking { int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; - GameMain.NetLobbyScreen.MissionType = (Barotrauma.MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); + GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; - if (traitorSetting < 0) traitorSetting = 2; - if (traitorSetting > 2) traitorSetting = 0; + if (traitorSetting < 0) { traitorSetting = 2; } + if (traitorSetting > 2) { traitorSetting = 0; } TraitorsEnabled = (YesNoMaybe)traitorSetting; int botCount = BotCount + incMsg.ReadByte() - 1; - if (botCount < 0) botCount = MaxBotCount; - if (botCount > MaxBotCount) botCount = 0; + if (botCount < 0) { botCount = MaxBotCount; } + if (botCount > MaxBotCount) { botCount = 0; } BotCount = botCount; int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; - if (botSpawnMode < 0) botSpawnMode = 1; - if (botSpawnMode > 1) botSpawnMode = 0; + if (botSpawnMode < 0) { botSpawnMode = 1; } + if (botSpawnMode > 1) { botSpawnMode = 0; } BotSpawnMode = (BotSpawnMode)botSpawnMode; float levelDifficulty = incMsg.ReadSingle(); - if (levelDifficulty >= 0.0f) SelectedLevelDifficulty = levelDifficulty; + if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; } - UseRespawnShuttle = incMsg.ReadBoolean(); + bool changedUseRespawnShuttle = incMsg.ReadBoolean(); + bool useRespawnShuttle = incMsg.ReadBoolean(); + if (changedUseRespawnShuttle) + { + UseRespawnShuttle = useRespawnShuttle; + } bool changedAutoRestart = incMsg.ReadBoolean(); bool autoRestart = incMsg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index ff031da8e..be597c2da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -216,6 +216,14 @@ namespace Barotrauma } } + public void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) + { + foreach (Client client in connectedClients) + { + client.ResetVotes(resetKickVotes); + } + } + public void ServerRead(IReadMessage inc, Client sender) { if (GameMain.Server == null || sender == null) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index e746fac64..ec2ca5d40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -192,7 +192,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients); + GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index f4a37340a..18e6c142d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -53,12 +53,13 @@ namespace Barotrauma.Steam Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); - Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText); + Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); - Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString()); + Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); + Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp + => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 319a53b07..68ab0885b 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.3.0 + 0.19.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 740c80b72..5dc043af1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -527,7 +527,7 @@ namespace Barotrauma { // We'll want this to run each time, because the delegate is used to find a valid button component. bool canAccessButtons = false; - foreach (var button in door.Item.GetConnectedComponents(true)) + foreach (var button in door.Item.GetConnectedComponents(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state")) { if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) { @@ -675,6 +675,8 @@ namespace Barotrauma } } float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); + //heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them + if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; } if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item)) { closestButton = button; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index f2d68c7c8..b524580df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1634,9 +1634,9 @@ namespace Barotrauma return id; } - public static void ApplyHealthData(Character character, XElement healthData) + public static void ApplyHealthData(Character character, XElement healthData, Func afflictionPredicate = null) { - if (healthData != null) { character?.CharacterHealth.Load(healthData); } + if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index f41b7720f..cb1b33763 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -12,12 +12,8 @@ namespace Barotrauma { public readonly static PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); Character.RemoveByPrefab(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs index ae592923a..f34b8ae11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs @@ -11,13 +11,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } public static CorpsePrefab Get(Identifier identifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 58c1de0d0..685429eb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1227,7 +1227,7 @@ namespace Barotrauma } } - public void Load(XElement element) + public void Load(XElement element, Func afflictionPredicate = null) { foreach (var subElement in element.Elements()) { @@ -1260,6 +1260,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found."); return; } + if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; } float strength = afflictionElement.GetAttributeFloat("strength", 0.0f); var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab); if (irremovableAffliction != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index b0df4f80d..2db2aaadf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -65,13 +65,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } private static readonly Dictionary _itemRepairPriorities = new Dictionary(); /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 2e505890e..d8f954873 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -56,11 +56,6 @@ namespace Barotrauma } } - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index cd0906887..b57133514 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -40,7 +40,9 @@ namespace Barotrauma } catch { - prefab.Dispose(); //clean up before rethrowing, since some prefab types might lock resources + //clean up before rethrowing, since some prefab types might lock resources + prefab.Dispose(); + Prefabs.Remove(prefab); throw; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 1cfef53ce..d99d29b57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -29,25 +29,25 @@ namespace Barotrauma public readonly ImmutableArray AltNames; public readonly string Path; public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; - public readonly UInt64 SteamWorkshopId; + public readonly Option UgcId; public readonly Version GameVersion; public readonly string ModVersion; public Md5Hash Hash { get; private set; } - public readonly DateTime? InstallTime; + public readonly Option InstallTime; public ImmutableArray Files { get; private set; } public ImmutableArray Errors { get; private set; } public async Task IsUpToDate() { - if (SteamWorkshopId != 0 && InstallTime.HasValue) - { - Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId); - if (item is null) { return true; } - return item.Value.LatestUpdateTime <= InstallTime; - } - return true; + if (!UgcId.TryUnwrap(out var ugcId)) { return true; } + if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; } + if (!InstallTime.TryUnwrap(out var installTime)) { return true; } + + Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); + if (item is null) { return true; } + return item.Value.LatestUpdateTime <= installTime; } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); @@ -66,18 +66,19 @@ namespace Barotrauma AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) .Select(n => n.Trim()).ToImmutableArray(); AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); - SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + UgcId = steamWorkshopId != 0 + ? Option.Some(new SteamWorkshopId(steamWorkshopId)) + : Option.None(); GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); - if (rootElement.Attribute("installtime") != null) - { - InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0)); - } - else - { - InstallTime = null; - } + UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); + InstallTime = installTimeUnix != 0 + ? Option.Some(ToolBox.Epoch.ToDateTime(installTimeUnix)) + : Option.None(); var fileResults = rootElement.Elements() .Select(e => ContentFile.CreateFromXElement(this, e)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs new file mode 100644 index 000000000..6a9f22ba3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/ContentPackageId.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Barotrauma +{ + public abstract class ContentPackageId + { + public abstract string StringRepresentation { get; } + + public override string ToString() + => StringRepresentation; + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + public static Option Parse(string s) + => ReflectionUtils.ParseDerived(s); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs new file mode 100644 index 000000000..6e8411d9d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackageId/SteamWorkshopId.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; +using System.Globalization; + +namespace Barotrauma +{ + sealed class SteamWorkshopId : ContentPackageId + { + public readonly UInt64 Value; + + public SteamWorkshopId(UInt64 value) + { + Value = value; + } + + private const string Prefix = "STEAM_WORKSHOP_"; + + public override string StringRepresentation => Value.ToString(CultureInfo.InvariantCulture); + + public override bool Equals(object? obj) + => obj is SteamWorkshopId otherWorkshopId && otherWorkshopId.Value == Value; + + public override int GetHashCode() => Value.GetHashCode(); + + public new static Option Parse(string s) + { + if (s.StartsWith(Prefix)) { s = s[Prefix.Length..]; } + if (!UInt64.TryParse(s, out var id) || id == 0) { return Option.None(); } + return Option.Some(new SteamWorkshopId(id)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 6eb09305c..11862c85b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -181,7 +181,7 @@ namespace Barotrauma { if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) { - SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ?? + SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ?? ContentPackageManager.CorePackages.First()); } @@ -193,7 +193,7 @@ namespace Barotrauma newRegular.Add(p); } else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 - => p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP) + => p2.UgcId == p.UgcId) is { } newP) { newRegular.Add(newP); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index b7388bb2b..4e0a4328b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -43,10 +43,10 @@ namespace Barotrauma cachedValue = cachedValue .Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase) .Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase); - if (ContentPackage.SteamWorkshopId != 0) + if (ContentPackage.UgcId.TryUnwrap(out var ugcId)) { cachedValue = cachedValue - .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); + .Replace(string.Format(OtherModDirFmt, ugcId.StringRepresentation), modPath, StringComparison.OrdinalIgnoreCase); } } var allPackages = ContentPackageManager.AllPackages; @@ -55,9 +55,9 @@ namespace Barotrauma #endif foreach (Identifier otherModName in otherMods) { - if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } + Option ugcId = ContentPackageId.Parse(otherModName.Value); ContentPackage? otherMod = - allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId) + allPackages.FirstOrDefault(p => ugcId == p.UgcId) ?? allPackages.FirstOrDefault(p => p.Name == otherModName) ?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName)) ?? throw new MissingContentPackageException(ContentPackage, otherModName.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index 8b3842223..e381b89ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -278,6 +278,19 @@ namespace Barotrauma } } } + + public static void ListCoroutines() + { + lock (Coroutines) + { + DebugConsole.NewMessage("***********"); + DebugConsole.NewMessage($"{Coroutines.Count} coroutine(s)"); + foreach (var c in Coroutines) + { + DebugConsole.NewMessage($"- {c.Name}"); + } + } + } } class WaitForSeconds : CoroutineStatus diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index ad6fcde98..3899632f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1651,6 +1651,8 @@ namespace Barotrauma }, isCheat: false)); commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); + + commands.Add(new Command("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); })); commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index cd9087d16..1cda074e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -28,10 +28,7 @@ namespace Barotrauma DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); } - static bool IsTargetTagAttribute(XAttribute attribute) - { - return attribute.Name.ToString().Equals("targettag", System.StringComparison.OrdinalIgnoreCase); - } + static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag"; } private string GetEventName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs new file mode 100644 index 000000000..f2d7af787 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs @@ -0,0 +1,65 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma; + +class CheckConnectionAction : BinaryOptionAction +{ + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ConnectionName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ConnectedItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier OtherConnectionName { get; set; } + + public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + var connectTargets = !ConnectedItemTag.IsEmpty ? ParentEvent.GetTargets(ConnectedItemTag) : Enumerable.Empty(); + foreach (var target in ParentEvent.GetTargets(ItemTag)) + { + if (target is not Item targetItem) { continue; } + if (targetItem.GetComponent() is not ConnectionPanel panel) { continue; } + if (panel.Connections == null || panel.Connections.None()) { continue; } + foreach (var connection in panel.Connections) + { + if (!IsCorrectConnection(connection, ConnectionName)) { continue; } + if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty) + { + if (connection.Wires.Any()) { return true; } + continue; + } + foreach (var wire in connection.Wires) + { + if (wire.OtherConnection(connection) is not Connection otherConnection) { continue; } + if (ConnectedItemTag.IsEmpty) + { + if (IsCorrectConnection(otherConnection, OtherConnectionName)) { return true; } + } + else if (OtherConnectionName.IsEmpty) + { + if (IsCorrectItem()) { return true; } + } + else + { + if (!IsCorrectConnection(otherConnection, OtherConnectionName)) { continue; } + if (!IsCorrectItem()) { continue; } + return true; + } + + bool IsCorrectItem() => connectTargets.Contains(otherConnection.Item); + } + + bool IsCorrectConnection(Connection connection, Identifier id) => connection.Name.ToIdentifier() == id; + } + } + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index b7874d26a..dba8364ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -14,6 +14,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string ItemTags { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool RequireEquipped { get; set; } private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; @@ -30,20 +33,24 @@ namespace Barotrauma if (!targets.Any()) { return null; } foreach (var target in targets) { - if (!(target is Character chr)) { continue; } - if (chr.Inventory == null) { continue; } - - if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; } - - foreach (var identifier in itemIdentifierSplit) + if (target is Character character) { - if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null) + if (RequireEquipped) { - return true; + if (itemTags.Any(tag => character.HasEquippedItem(tag))) { return true; } + if (itemIdentifierSplit.Any(identifier => character.HasEquippedItem(identifier))) { return true; } + return false; } + if (character.Inventory is not CharacterInventory inventory) { continue; } + if (itemTags.Any(tag => inventory.FindItemByTag(tag, recursive: true) is not null)) { return true; } + if (itemIdentifierSplit.Any(identifier => inventory.FindItemByIdentifier(identifier, recursive: true) is not null)) { return true; } + } + else if (target is Item item && item.OwnInventory is ItemInventory inventory) + { + if (itemTags.Any(tag => inventory.FindItemByTag(tag, recursive: true) is not null)) { return true; } + if (itemIdentifierSplit.Any(identifier => inventory.FindItemByIdentifier(identifier, recursive: true) is not null)) { return true; } } } - return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 8413b6746..0803242f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -15,40 +15,36 @@ namespace Barotrauma protected override bool? DetermineSuccess() { - ISerializableEntity target = null; + Character targetCharacter = null; if (!TargetTag.IsEmpty) { foreach (var t in ParentEvent.GetTargets(TargetTag)) { - if (t is ISerializableEntity e) + if (t is Character c) { - target = e; + targetCharacter = c; break; } } } - if (target == null) + if (targetCharacter == null) { - DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); - return true; - } - if (target is Character character) - { - var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Identifier == OrderIdentifier) - { - if (OrderOption.IsEmpty) - { - return true; - } - else - { - return currentOrderInfo?.Option == OrderOption; - } - } + DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); return false; } - return true; + var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority(); + if (currentOrderInfo?.Identifier == OrderIdentifier) + { + if (OrderOption.IsEmpty) + { + return true; + } + else + { + return currentOrderInfo?.Option == OrderOption; + } + } + return false; } private string GetEventName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs new file mode 100644 index 000000000..1c9903f6a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs @@ -0,0 +1,89 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; + +namespace Barotrauma +{ + class CheckSelectedItemAction : BinaryOptionAction + { + public enum SelectedItemType { Primary, Secondary, Any }; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CharacterTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] + public SelectedItemType ItemType { get; set; } + + public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + Character character = null; + if (!CharacterTag.IsEmpty) + { + foreach (var t in ParentEvent.GetTargets(CharacterTag)) + { + if (t is Character c) + { + character = c; + break; + } + } + } + if (character == null) + { + DebugConsole.ShowError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail."); + return false; + } + if (!TargetTag.IsEmpty) + { + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + if (targets.None()) + { + DebugConsole.ShowError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + return false; + } + foreach (var target in targets) + { + if (target is not Item targetItem) + { + continue; + } + if (IsSelected(targetItem)) + { + return true; + } + } + return false; + + bool IsSelected(Item item) + { + return ItemType switch + { + SelectedItemType.Any => character.IsAnySelectedItem(item), + SelectedItemType.Primary => character.SelectedItem == item, + SelectedItemType.Secondary => character.SelectedSecondaryItem == item, + _ => false + }; + } + } + else + { + return ItemType switch + { + SelectedItemType.Any => !character.HasSelectedAnyItem, + SelectedItemType.Primary => character.SelectedItem == null, + SelectedItemType.Secondary => character.SelectedSecondaryItem == null, + _ => false + }; + } + } + + private string GetEventName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs new file mode 100644 index 000000000..b974c6f83 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs @@ -0,0 +1,49 @@ +#nullable enable + +namespace Barotrauma +{ + internal sealed class CheckTalentAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TalentIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + if (TargetTag.IsEmpty) + { + return false; + } + + Character? matchingCharacter = null; + + foreach (Entity entity in ParentEvent.GetTargets(TargetTag)) + { + if (entity is Character character) + { + matchingCharacter = character; + break; + } + } + + return matchingCharacter is not null && matchingCharacter.HasTalent(TalentIdentifier); + } + + public override string ToDebugString() + { + string subActionStr = ""; + if (succeeded.HasValue) + { + subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; + } + + return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckTalentAction)} -> (Talent: {TalentIdentifier.ColorizeObject()}" + + $" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + + subActionStr; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 1577c7514..2056c9656 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -1,10 +1,18 @@ -using System; -using System.Linq; - namespace Barotrauma { - class MessageBoxAction : EventAction + partial class MessageBoxAction : EventAction { + public enum ActionType { Create, Close } + + [Serialize(ActionType.Create, IsPropertySaveable.Yes)] + public ActionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] public Identifier Header { get; set; } @@ -24,7 +32,7 @@ namespace Barotrauma public string CloseOnInput { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier CloseOnInteractTag { get; set; } + public Identifier CloseOnSelectTag { get; set; } [Serialize("", IsPropertySaveable.Yes)] public Identifier CloseOnPickUpTag { get; set; } @@ -35,8 +43,11 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier CloseOnExitRoomName { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] - public bool IsTutorialObjective { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CloseOnInRoomName { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ObjectiveTag { get; set; } private bool isFinished = false; @@ -45,72 +56,16 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } -#if CLIENT - CreateMessageBox(); - if (IsTutorialObjective && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) - { - tutorialMode.Tutorial?.TriggerTutorialSegment(new Tutorials.Tutorial.Segment(Text, CreateMessageBox)); - } -#endif + UpdateProjSpecific(); isFinished = true; } -#if CLIENT - public void CreateMessageBox() - { - new GUIMessageBox( - headerText: TextManager.Get(Header), - text: RichString.Rich(TextManager.ParseInputTypes(TextManager.Get(Text).Fallback(Text.ToString()), useColorHighlight: true)), - buttons: Array.Empty(), - type: GUIMessageBox.Type.Tutorial, - iconStyle: IconStyle, - autoCloseCondition: GetAutoCloseCondition(), - hideCloseButton: HideCloseButton); - } -#endif + partial void UpdateProjSpecific(); - private Func GetAutoCloseCondition() - { - var character = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Character; - Func autoCloseCondition = null; - if (!string.IsNullOrEmpty(CloseOnInput) && Enum.TryParse(CloseOnInput, true, out InputType closeOnInput)) - { -#if CLIENT - autoCloseCondition = () => PlayerInput.KeyDown(closeOnInput); -#endif - } - else if (!CloseOnInteractTag.IsEmpty) - { - autoCloseCondition = () => character?.SelectedItem != null && character.SelectedItem.HasTag(CloseOnInteractTag); - } - else if (!CloseOnPickUpTag.IsEmpty) - { - autoCloseCondition = () => character?.Inventory != null && character.Inventory.FindItemByTag(CloseOnPickUpTag, recursive: true) != null; - } - else if (!CloseOnEquipTag.IsEmpty) - { - autoCloseCondition = () => character != null && character.HasEquippedItem(CloseOnEquipTag); - } - else if (!CloseOnExitRoomName.IsEmpty) - { - autoCloseCondition = () => character?.CurrentHull != null && character.CurrentHull.RoomName.ToIdentifier() != CloseOnExitRoomName; - } - return autoCloseCondition; - } + public override bool IsFinished(ref string goToLabel) => isFinished; - public override bool IsFinished(ref string goToLabel) - { - return isFinished; - } + public override void Reset() => isFinished = false; - public override void Reset() - { - isFinished = false; - } - - public override string ToDebugString() - { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MessageBoxAction)}"; - } + public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MessageBoxAction)}"; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs new file mode 100644 index 000000000..1aacece60 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs @@ -0,0 +1,48 @@ +namespace Barotrauma; + +class TeleportAction : EventAction +{ + public enum TeleportPosition { MainSub, Outpost } + + [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes)] + public TeleportPosition Position { get; set; } + + [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + public SpawnType SpawnType { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string SpawnPointTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + private bool isFinished; + + public TeleportAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + Submarine sub = Position switch + { + TeleportPosition.MainSub => Submarine.MainSub, + TeleportPosition.Outpost => GameMain.GameSession?.Level?.StartOutpost, + _ => null + }; + if (WayPoint.GetRandom(spawnType: SpawnType, sub: sub, spawnPointTag: SpawnPointTag) is WayPoint wp) + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is Character c) + { + c.TeleportTo(wp.WorldPosition); + } + } + } + isFinished = true; + } + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs new file mode 100644 index 000000000..eebac860d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs @@ -0,0 +1,27 @@ +namespace Barotrauma; + +partial class TutorialHighlightAction : EventAction +{ + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool State { get; set; } + + private bool isFinished; + + public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + UpdateProjSpecific(); + isFinished = true; + } + + partial void UpdateProjSpecific(); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs new file mode 100644 index 000000000..317966346 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs @@ -0,0 +1,65 @@ +using System.Linq; + +namespace Barotrauma; + +class TutorialIconAction : EventAction +{ + public enum ActionType { Add, Remove, RemoveTarget, RemoveIcon, Clear }; + + [Serialize(ActionType.Add, IsPropertySaveable.Yes)] + public ActionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string IconStyle { get; set; } + + private bool isFinished; + + public TutorialIconAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + if (ParentEvent.GetTargets(TargetTag).FirstOrDefault() is Entity target) + { + if (Type == ActionType.Add) + { + tutorialMode.Tutorial?.Icons.Add((target, IconStyle)); + } + else if(Type == ActionType.Remove) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target && i.iconStyle.Equals(IconStyle, System.StringComparison.OrdinalIgnoreCase)); + } + else if (Type == ActionType.RemoveTarget) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target); + } + else if (Type == ActionType.RemoveIcon) + { + tutorialMode.Tutorial?.Icons.RemoveAll(i => i.iconStyle.Equals(IconStyle, System.StringComparison.OrdinalIgnoreCase)); + } + else if (Type == ActionType.Clear) + { + tutorialMode.Tutorial?.Icons.Clear(); + } + } + } +#endif + isFinished = true; + } + + public override bool IsFinished(ref string goToLabel) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs index a7e6a7658..b447875bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs @@ -1,12 +1,8 @@ -#if CLIENT -using Barotrauma.Tutorials; -#endif - namespace Barotrauma { - class TutorialSegmentAction : EventAction + partial class TutorialSegmentAction : EventAction { - public enum SegmentActionType { Trigger, Complete, Remove }; + public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove }; [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] public SegmentActionType Type { get; set; } @@ -15,7 +11,7 @@ namespace Barotrauma public Identifier Id { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier ObjectiveTextTag { get; set; } + public Identifier ObjectiveTag { get; set; } [Serialize(false, IsPropertySaveable.Yes)] public bool AutoPlayVideo { get; set; } @@ -32,64 +28,21 @@ namespace Barotrauma [Serialize(80, IsPropertySaveable.Yes)] public int Height { get; set; } -#if CLIENT - private readonly Tutorial.Segment segment; -#endif private bool isFinished; - public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { -#if CLIENT - // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) - if (Type == SegmentActionType.Trigger) - { - segment = new Tutorial.Segment(Id, ObjectiveTextTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), - new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); - } -#endif - } + public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override void Update(float deltaTime) { if (isFinished) { return; } - -#if CLIENT - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) - { - if (tutorialMode.Tutorial is Tutorial tutorial) - { - switch (Type) - { - case SegmentActionType.Trigger: - tutorial.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - tutorial.CompleteTutorialSegment(Id); - break; - case SegmentActionType.Remove: - tutorial.RemoveTutorialSegment(Id); - break; - } - } - } - else - { - DebugConsole.ShowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); - } -#endif - + UpdateProjSpecific(); isFinished = true; } - public override bool IsFinished(ref string goToLabel) - { - return isFinished; - } + partial void UpdateProjSpecific(); - public override void Reset() - { - isFinished = false; - } + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a8015d651..9b397d4be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -24,7 +24,7 @@ namespace Barotrauma } #endif - class EventSet : Prefab + sealed class EventSet : Prefab { internal class EventDebugStats { @@ -489,12 +489,6 @@ namespace Barotrauma } } - public override void Dispose() - { - foreach (var childSet in ChildSets) - { - childSet.Dispose(); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 4039e31dc..9ea8ff2a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -298,5 +298,15 @@ namespace Barotrauma.Extensions return null; } + + public static IEnumerable NotNull(this IEnumerable source) where T : struct + => source + .Where(nullable => nullable.HasValue) + .Select(nullable => nullable.Value); + + public static IEnumerable NotNone(this IEnumerable> source) + => source + .OfType>() + .Select(some => some.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index 7b2e4ab9f..3b56fd1aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { @@ -7,14 +8,14 @@ namespace Barotrauma { public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s; - public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s); - public static bool IsNullOrWhiteSpace(this string? s) => string.IsNullOrWhiteSpace(s); - public static bool IsNullOrEmpty(this ContentPath? p) => p?.IsNullOrEmpty() ?? true; - public static bool IsNullOrWhiteSpace(this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; - public static bool IsNullOrEmpty(this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); - public static bool IsNullOrWhiteSpace(this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); - public static bool IsNullOrEmpty(this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); - public static bool IsNullOrWhiteSpace(this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs index d1f5112a8..ad6dec773 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs @@ -5,7 +5,13 @@ namespace Barotrauma { class TutorialPrefab : Prefab { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public static readonly PrefabCollection Prefabs = +#if CLIENT + new PrefabCollection(onSort: MainMenuScreen.UpdateInstanceTutorialButtons); +#else + new PrefabCollection(); +#endif public readonly int Order; public readonly bool DisableBotConversations; @@ -54,8 +60,11 @@ namespace Barotrauma return null; } Identifier speciesName = tutorialCharacterElement.GetAttributeIdentifier("speciesname", CharacterPrefab.HumanSpeciesName); - string jobPrefabIdentifier = tutorialCharacterElement.GetAttributeString("jobidentifier", "assistant"); - var jobPrefab = JobPrefab.Prefabs.FirstOrDefault(p => p.Identifier == jobPrefabIdentifier) ?? JobPrefab.Prefabs.First(); + Identifier jobPrefabIdentifier = tutorialCharacterElement.GetAttributeIdentifier("jobidentifier", "assistant"); + if (!JobPrefab.Prefabs.TryGet(jobPrefabIdentifier, out var jobPrefab)) + { + jobPrefab = JobPrefab.Prefabs.First(); + } int jobVariant = tutorialCharacterElement.GetAttributeInt("variant", 0); var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: jobPrefab, variant: jobVariant); foreach (var skillElement in tutorialCharacterElement.GetChildElements("skill")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 44065d74e..07d3b598c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -64,6 +64,8 @@ namespace Barotrauma.Items.Components private const float MinZoom = 1.0f, MaxZoom = 4.0f; private float zoom = 1.0f; + /// Accessed through event actions. Do not remove even if there are no references in code. + public bool UseDirectionalPing => useDirectionalPing; private bool useDirectionalPing = false; private Vector2 pingDirection = new Vector2(1.0f, 0.0f); private bool useMineralScanner; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index bd3140234..3803e5b16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -82,9 +82,14 @@ namespace Barotrauma.Items.Components public sealed override void Update(float deltaTime, Camera cam) { int receivedInputs = 0; + bool allInputsTimedOut = true; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } + if (timeSinceReceived[i] <= timeFrame) + { + allInputsTimedOut = false; + receivedInputs += 1; + } timeSinceReceived[i] += deltaTime; } @@ -93,7 +98,7 @@ namespace Barotrauma.Items.Components if (string.IsNullOrEmpty(signalOut)) { //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } + if (!state && allInputsTimedOut) { IsActive = false; } return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 5e70fcf9d..10fec01b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -58,6 +58,9 @@ namespace Barotrauma.Items.Components } } + public bool WaterDetected => isInWater; + public int WaterPercentage => GetWaterPercentage(item.CurrentHull); + public WaterDetector(Item item, ContentXElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c8fc6f8a2..278b4a102 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -2179,7 +2179,7 @@ namespace Barotrauma /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent + public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true, Func connectionFilter = null) where T : ItemComponent { List connectedComponents = new List(); @@ -2195,6 +2195,7 @@ namespace Barotrauma foreach (Connection c in connectionPanel.Connections) { + if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; } var recipients = c.Recipients; foreach (Connection recipient in recipients) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index ce8d957e2..79d80014d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -807,9 +807,6 @@ namespace Barotrauma //only used if the item doesn't have a name/description defined in the currently selected language string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", ""); - //works the same as nameIdentifier, but just replaces the description - Identifier descriptionIdentifier = ConfigElement.GetAttributeIdentifier("descriptionidentifier", ""); - name = TextManager.Get(nameIdentifier.IsEmpty ? $"EntityName.{Identifier}" : $"EntityName.{nameIdentifier}", @@ -858,18 +855,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); - if (descriptionIdentifier != Identifier.Empty) - { - Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}").Fallback(Description); - } - else if (nameIdentifier == Identifier.Empty) - { - Description = TextManager.Get($"EntityDescription.{Identifier}").Fallback(Description); - } - else - { - Description = TextManager.Get($"EntityDescription.{nameIdentifier}").Fallback(Description); - } + LoadDescription(ConfigElement); var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); @@ -1320,12 +1306,8 @@ namespace Barotrauma throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance"); } - private bool disposed = false; public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); Item.RemoveByPrefab(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index c5c646150..193e7fdf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -84,7 +84,6 @@ namespace Barotrauma } } - private bool disposed = false; public override Sprite Sprite => null; @@ -102,9 +101,8 @@ namespace Barotrauma public override void Dispose() { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); + throw new InvalidOperationException( + $"{nameof(CoreEntityPrefab)}.{nameof(Dispose)} should never be called"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 574115e1f..ab5c17c8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -157,7 +157,7 @@ namespace Barotrauma public void Delete() { - Dispose(); + Prefabs.Remove(this); try { if (ContentPackage is { Files: { Length: 1 } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 133da33e1..4daa4282a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -238,8 +238,8 @@ namespace Barotrauma //find the edge at the opposite side of the adjacent cell foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges) { - if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) < 0 && - otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType == CellType.Solid) + if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 && + otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { adjacentEdge = otherEdge; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index a1de6a144..46d549e2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -11,15 +11,7 @@ namespace Barotrauma { partial class LinkedSubmarinePrefab : MapEntityPrefab { - //public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - //Prefabs.Remove(this); - } + public override void Dispose() { } public readonly SubmarineInfo subInfo; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 08ac7d07d..d90c90e99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -1253,7 +1253,7 @@ namespace Barotrauma { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } - return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); + return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } public void Discover(bool checkTalents = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 258e7262f..bdec8d1b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -280,5 +280,29 @@ namespace Barotrauma return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier) || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } + + protected void LoadDescription(ContentXElement element) + { + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); + + string originalDescription = Description.Value; + if (descriptionIdentifier != Identifier.Empty) + { + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); + } + else if (nameIdentifier == Identifier.Empty) + { + Description = TextManager.Get($"EntityDescription.{Identifier}"); + } + else + { + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); + } + if (!originalDescription.IsNullOrEmpty()) + { + Description = Description.Fallback(originalDescription); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index d13d4216f..3dd5eea35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -142,8 +142,6 @@ namespace Barotrauma //only used if the item doesn't have a name/description defined in the currently selected language Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", ""); - Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - Name = TextManager.Get(nameIdentifier.IsEmpty ? $"EntityName.{Identifier}" : $"EntityName.{nameIdentifier}", @@ -271,18 +269,7 @@ namespace Barotrauma tags.Add("wall".ToIdentifier()); } - if (!descriptionIdentifier.IsEmpty) - { - Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}").Fallback(Description); - } - else if (nameIdentifier.IsEmpty) - { - Description = TextManager.Get($"EntityDescription.{Identifier}").Fallback(Description); - } - else - { - Description = TextManager.Get($"EntityDescription.{nameIdentifier}").Fallback(Description); - } + LoadDescription(element); //backwards compatibility if (element.GetAttribute("size") == null) @@ -331,12 +318,6 @@ namespace Barotrauma throw new NotImplementedException(); } - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 50e3f4507..6ffee8449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1548,6 +1548,7 @@ namespace Barotrauma element.Add(new XAttribute("description", Info.Description ?? "")); element.Add(new XAttribute("checkval", Rand.Int(int.MaxValue))); element.Add(new XAttribute("price", Info.Price)); + element.Add(new XAttribute("tier", Info.Tier)); element.Add(new XAttribute("initialsuppliesspawned", Info.InitialSuppliesSpawned)); element.Add(new XAttribute("noitems", Info.NoItems)); element.Add(new XAttribute("lowfuel", !CheckFuel())); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 1d7e30e5e..84fb06106 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -205,15 +205,7 @@ namespace Barotrauma.Networking { votes[(int)voteType] = value; } - - public void ResetVotes() - { - for (int i = 0; i < votes.Length; i++) - { - votes[i] = null; - } - } - + public bool SessionOrAccountIdMatches(string userId) => (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId) || (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 7e93a540c..a1505a420 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -131,28 +131,29 @@ namespace Barotrauma.Networking enum DisconnectReason { + //do not attempt reconnecting with these reasons Unknown, + Disconnected, Banned, Kicked, ServerShutdown, ServerCrashed, ServerFull, AuthenticationRequired, - SteamAuthenticationRequired, SteamAuthenticationFailed, SessionTaken, TooManyFailedLogins, - NoName, InvalidName, NameTaken, InvalidVersion, - MissingContentPackage, - IncompatibleContentPackage, + SteamP2PError, + + //attempt reconnecting with these reasons + Timeout, ExcessiveDesyncOldEvent, ExcessiveDesyncRemovedEvent, SyncTimeout, - SteamP2PError, - SteamP2PTimeOut, + SteamP2PTimeOut } abstract partial class NetworkMember @@ -163,71 +164,38 @@ namespace Barotrauma.Networking set; } - public virtual bool IsServer - { - get { return false; } - } + public abstract bool IsServer { get; } - public virtual bool IsClient - { - get { return false; } - } + public abstract bool IsClient { get; } public abstract void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null); -#if DEBUG - public Dictionary messageCount = new Dictionary(); -#endif - - protected ServerSettings serverSettings; + public abstract Voting Voting { get; } - public Voting Voting { get; protected set; } - - protected TimeSpan updateInterval; protected DateTime updateTimer; - protected bool gameStarted; - - protected RespawnManager respawnManager; - public bool ShowNetStats; public float SimulatedRandomLatency, SimulatedMinimumLatency; public float SimulatedLoss; public float SimulatedDuplicatesChance; - public int TickRate - { - get { return serverSettings.TickRate; } - set - { - serverSettings.TickRate = MathHelper.Clamp(value, 1, 60); - updateInterval = new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / serverSettings.TickRate, 1, 500)); - } - } - public KarmaManager KarmaManager { get; private set; } = new KarmaManager(); - public bool GameStarted - { - get { return gameStarted; } - } + public bool GameStarted { get; protected set; } public abstract IReadOnlyList ConnectedClients { get; } - public RespawnManager RespawnManager - { - get { return respawnManager; } - } + public RespawnManager RespawnManager { get; protected set; } + + public ServerSettings ServerSettings { get; protected set; } + + public TimeSpan UpdateInterval => new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / ServerSettings.TickRate, 1, 500)); - public ServerSettings ServerSettings - { - get { return serverSettings; } - } public bool CanUseRadio(Character sender) { @@ -277,24 +245,6 @@ namespace Barotrauma.Networking public abstract void UnbanPlayer(Endpoint endpoint); - public virtual void Update(float deltaTime) { } - - public virtual void Quit() { } - - /// - /// Check if the two version are compatible (= if they can play together in multiplayer). - /// Returns null if compatibility could not be determined (invalid/unknown version number). - /// - public static bool? IsCompatible(string myVersion, string remoteVersion) - { - if (string.IsNullOrEmpty(myVersion) || string.IsNullOrEmpty(remoteVersion)) { return null; } - - if (!Version.TryParse(myVersion, out Version myVersionNumber)) { return null; } - if (!Version.TryParse(remoteVersion, out Version remoteVersionNumber)) { return null; } - - return IsCompatible(myVersionNumber, remoteVersionNumber); - } - /// /// 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 index 3be504afe..47fc84fd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Networking public abstract string StringRepresentation { get; } public static Option Parse(string str) - => ReflectionUtils.ParseDerived(str); + => ReflectionUtils.ParseDerived(str); public abstract override bool Equals(object? obj); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs index 0724a0bca..f5ca6da14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Networking public abstract string StringRepresentation { get; } public static Option
Parse(string str) - => ReflectionUtils.ParseDerived
(str); + => ReflectionUtils.ParseDerived(str); public abstract bool IsLocalHost { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs index 3a724de81..5fadd7644 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs @@ -17,31 +17,27 @@ namespace Barotrauma.Networking public LidgrenAddress(IPAddress netAddress) { - NetAddress = netAddress; + if (IPAddress.IsLoopback(netAddress)) + { + NetAddress = IPAddress.Loopback; + } + else + { + NetAddress = netAddress; + } } public new static Option Parse(string endpointStr) { - if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint)) + if (endpointStr.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return Option.Some(new LidgrenAddress(IPAddress.Loopback)); + } + else if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint)) { return Option.Some(new LidgrenAddress(netEndpoint!)); } - - 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(); - } + return Option.None(); } public override bool Equals(object? obj) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs index 6880848c4..f1599e654 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs @@ -22,12 +22,21 @@ namespace Barotrauma.Networking public override string ToString() => StringRepresentation; public static Option Parse(string str) - => ReflectionUtils.ParseDerived(str); + => ReflectionUtils.ParseDerived(str); - public static bool operator ==(Endpoint a, Endpoint b) - => a.Equals(b); + public static bool operator ==(Endpoint? a, Endpoint? b) + { + if (a is null) + { + return b is null; + } + else + { + return a.Equals(b); + } + } - public static bool operator !=(Endpoint a, Endpoint b) + 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 index 32e85d169..44d20264a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -24,24 +24,23 @@ namespace Barotrauma.Networking public new static Option Parse(string endpointStr) { - if (IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint)) - { - return Option.Some(new LidgrenEndpoint(netEndpoint!)); - } - + string hostName = endpointStr; + int port = NetConfig.DefaultPort; 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)); - } + hostName = split[0]; + port = int.TryParse(split[1], out var tmpPort) ? tmpPort : port; } - - return LidgrenAddress.Parse(endpointStr) - .Select(adr => new LidgrenEndpoint(adr.NetAddress, NetConfig.DefaultPort)); + + if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr)) + { + return Option.Some(new LidgrenEndpoint(adr.NetAddress, port)); + } + + return IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint) + ? Option.Some(new LidgrenEndpoint(netEndpoint)) + : Option.None(); } public override bool Equals(object? obj) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 6ae0d0e1b..893bed235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -1,8 +1,8 @@ #nullable enable - using System; using System.Collections.Immutable; using System.Linq; +using System.Text; namespace Barotrauma.Networking { @@ -68,7 +68,7 @@ namespace Barotrauma.Networking public byte[] Buffer; public readonly int Length => Buffer.Length; - public readonly IReadMessage GetReadMessage() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false); + public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false); public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn); } @@ -86,9 +86,164 @@ namespace Barotrauma.Networking } [NetworkSerialize] - internal struct PeerDisconnectPacket : INetSerializableStruct + internal readonly struct PeerDisconnectPacket : INetSerializableStruct { - public string Message; + public readonly DisconnectReason DisconnectReason; + + public readonly string AdditionalInformation; + + private PeerDisconnectPacket( + DisconnectReason disconnectReason, + string additionalInformation = "") + { + DisconnectReason = disconnectReason; + AdditionalInformation = additionalInformation; + } + + public LocalizedString ChatMessage(Client c) + => DisconnectReason switch + { + DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer", + "[client]", c.Name), + _ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason", + ("[client]", c.Name), + ("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}"))) + }; + + private LocalizedString msgWithReason + => TextManager.Get($"DisconnectReason.{DisconnectReason}") + + "\n\n" + + TextManager.Get("banreason") + " " + AdditionalInformation; + + private LocalizedString serverMessage + => TextManager.Get($"ServerMessage.{DisconnectReason}"); + + public LocalizedString PopupMessage + => DisconnectReason switch + { + DisconnectReason.Banned => msgWithReason, + DisconnectReason.Kicked => msgWithReason, + DisconnectReason.InvalidVersion => TextManager.GetWithVariables("DisconnectMessage.InvalidVersion", + ("[version]", AdditionalInformation), + ("[clientversion]", GameMain.Version.ToString())), + DisconnectReason.ExcessiveDesyncOldEvent => serverMessage, + DisconnectReason.ExcessiveDesyncRemovedEvent => serverMessage, + DisconnectReason.SyncTimeout => serverMessage, + _ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost")) + }; + + public LocalizedString ReconnectMessage + => PopupMessage + "\n\n" + TextManager.Get("ConnectionLostReconnecting"); + + public PlayerConnectionChangeType ConnectionChangeType + => DisconnectReason switch + { + DisconnectReason.Banned => PlayerConnectionChangeType.Banned, + DisconnectReason.Kicked => PlayerConnectionChangeType.Kicked, + _ => PlayerConnectionChangeType.Disconnected + }; + + public bool ShouldAttemptReconnect + => DisconnectReason + is DisconnectReason.ExcessiveDesyncOldEvent + or DisconnectReason.ExcessiveDesyncRemovedEvent + or DisconnectReason.Timeout + or DisconnectReason.SyncTimeout + or DisconnectReason.SteamP2PTimeOut; + + public bool IsEventSyncError + => DisconnectReason + is DisconnectReason.ExcessiveDesyncOldEvent + or DisconnectReason.ExcessiveDesyncRemovedEvent + or DisconnectReason.SyncTimeout; + + public bool ShouldCreateAnalyticsEvent + => DisconnectReason is not ( + DisconnectReason.Disconnected + or DisconnectReason.Banned + or DisconnectReason.Kicked + or DisconnectReason.TooManyFailedLogins + or DisconnectReason.InvalidVersion); + + public bool ShouldShowMessage + => DisconnectReason is not DisconnectReason.Disconnected; + + private const string lidgrenSeparator = ":hankey:"; + + /// + /// This exists because Lidgren is a piece of shit and + /// doesn't readily support sending anything other than + /// a string through a disconnect packet, so this thing + /// needs a sufficiently nasty string representation that + /// can be decoded with some certainty that it won't get + /// mangled by user input. + /// + public string ToLidgrenStringRepresentation() + { + static string strToBase64(string str) + => Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); + + return DisconnectReason + + lidgrenSeparator + + strToBase64(AdditionalInformation); + } + + public static Option FromLidgrenStringRepresentation(string str) + { + // Lidgren has some hardcoded disconnect strings that it uses + // when it detects that a connection has failed. We can handle + // timeouts, so let's look for strings related to that and return + // an appropriate PeerDisconnectPacket. + switch (str) + { + case Lidgren.Network.NetConnection.NoResponseMessage: + case "Connection timed out": + case "Reconnecting": + return Option.Some(WithReason(DisconnectReason.Timeout)); + } + + static string base64ToStr(string base64) + => Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + + string[] split = str.Split(lidgrenSeparator); + if (split.Length != 2) { return Option.None(); } + if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None(); } + return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1]))); + } + + public static PeerDisconnectPacket Custom(string customMessage) + => new PeerDisconnectPacket( + DisconnectReason.Unknown, + customMessage); + + public static PeerDisconnectPacket WithReason(DisconnectReason disconnectReason) + => new PeerDisconnectPacket(disconnectReason); + + public static PeerDisconnectPacket Kicked(string? msg) + => new PeerDisconnectPacket(DisconnectReason.Kicked, msg ?? ""); + + public static PeerDisconnectPacket Banned(string? msg) + => new PeerDisconnectPacket(DisconnectReason.Banned, msg ?? ""); + + public static PeerDisconnectPacket InvalidVersion() + => new PeerDisconnectPacket( + DisconnectReason.InvalidVersion, + GameMain.Version.ToString()); + + public static PeerDisconnectPacket SteamP2PError(Steamworks.P2PSessionError error) + => new PeerDisconnectPacket( + DisconnectReason.SteamP2PError, + error.ToString()); + + public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error) + => new PeerDisconnectPacket( + DisconnectReason.SteamAuthenticationFailed, + $"{nameof(Steamworks.BeginAuthResult)}.{error}"); + + public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error) + => new PeerDisconnectPacket( + DisconnectReason.SteamAuthenticationFailed, + $"{nameof(Steamworks.AuthResponse)}.{error}"); } // ReSharper disable MemberCanBePrivate.Global, FieldCanBeMadeReadOnly.Global, UnassignedField.Global @@ -101,11 +256,14 @@ namespace Barotrauma.Networking public byte[] HashBytes = Array.Empty(); [NetworkSerialize] - public ulong WorkshopId; + public string UgcId = ""; [NetworkSerialize] public uint InstallTimeDiffInSeconds; + [NetworkSerialize] + public bool IsMandatory; + private Md5Hash? cachedHash; private DateTime? cachedDateTime; @@ -130,9 +288,12 @@ namespace Barotrauma.Networking { Name = contentPackage.Name; Hash = contentPackage.Hash; - WorkshopId = contentPackage.SteamWorkshopId; + UgcId = contentPackage.UgcId.TryUnwrap(out var ugcId) + ? ugcId.StringRepresentation + : ""; + IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile); InstallTimeDiffInSeconds = - contentPackage.InstallTime is { } installTime + contentPackage.InstallTime.TryUnwrap(out var installTime) ? (uint)(installTime - referenceTime).TotalSeconds : 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 5aad66b97..6f68b3730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -333,10 +333,14 @@ namespace Barotrauma.Networking RespawnCharactersProjSpecific(shuttlePos); } + public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() + { + return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); + } + public static Affliction GetRespawnPenaltyAffliction() { - var respawnPenaltyAffliction = AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); - return respawnPenaltyAffliction?.Instantiate(10.0f); + return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); } public static void GiveRespawnPenaltyAffliction(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 74e9223ec..cbf0474b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -388,11 +388,12 @@ namespace Barotrauma.Networking public List ClientPermissions { get; private set; } = new List(); + private int tickRate = 20; [Serialize(20, IsPropertySaveable.Yes)] public int TickRate { - get; - set; + get { return tickRate; } + set { tickRate = MathHelper.Clamp(value, 1, 60); } } [Serialize(true, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index e26cb7e62..594171239 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -56,23 +56,5 @@ namespace Barotrauma return selected; } - - public void ResetVotes(IEnumerable connectedClients) - { - foreach (Client client in connectedClients) - { - client.ResetVotes(); - } -#if CLIENT - foreach (VoteType voteType in Enum.GetValues(typeof(VoteType))) - { - SetVoteCountYes(voteType, 0); - SetVoteCountNo(voteType, 0); - SetVoteCountMax(voteType, 0); - } - UpdateVoteTexts(connectedClients, VoteType.Mode); - UpdateVoteTexts(connectedClients, VoteType.Sub); -#endif - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs index c86e6050c..167725951 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; namespace Barotrauma { - public abstract class Prefab : IDisposable + public abstract class Prefab { public readonly static ImmutableHashSet Types; static Prefab() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 5c3a16671..93fa4d6a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -37,6 +37,14 @@ namespace Barotrauma OnRemoveOverrideFile = onRemoveOverrideFile; } + /// + /// Constructor with only the OnSort callback provided. + /// + public PrefabCollection(Action? onSort) : this() + { + OnSort = onSort; + } + /// /// Method to be called when calling Add(T prefab, bool override). /// If provided, the method is called only if Add succeeds. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs index 631543c0a..8a0bbb7b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs @@ -4,16 +4,20 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; +using Barotrauma.Threading; namespace Barotrauma { public class PrefabSelector : IEnumerable where T : notnull, Prefab { + private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(); + public T? BasePrefab { get { - lock (overrides) { return basePrefabInternal; } + using (new ReadLock(rwl)) { return basePrefabInternal; } } } @@ -21,51 +25,70 @@ namespace Barotrauma { get { - lock (overrides) { return activePrefabInternal; } + using (new ReadLock(rwl)) { return activePrefabInternal; } } } public void Add(T prefab, bool isOverride) { - lock (overrides) { AddInternal(prefab, isOverride); } + using (new WriteLock(rwl)) { AddInternal(prefab, isOverride); } } public void RemoveIfContains(T prefab) { - lock (overrides) { RemoveIfContainsInternal(prefab); } + using (new WriteLock(rwl)) { RemoveIfContainsInternal(prefab); } } public void Remove(T prefab) { - lock (overrides) { RemoveInternal(prefab); } + using (new WriteLock(rwl)) { RemoveInternal(prefab); } } public void RemoveByFile(ContentFile file, Action? callback = null) { - lock (overrides) { RemoveByFileInternal(file, callback); } + var removed = new List(); + using (new WriteLock(rwl)) + { + for (int i = overrides.Count-1; i >= 0; i--) + { + var prefab = overrides[i]; + if (prefab.ContentFile == file) + { + RemoveInternal(prefab); + removed.Add(prefab); + } + } + + if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) + { + RemoveInternal(basePrefabInternal); + removed.Add(p); + } + } + if (callback != null) { removed.ForEach(callback); } } public void Sort() { - lock (overrides) { SortInternal(); } + using (new WriteLock(rwl)) { SortInternal(); } } public bool IsEmpty { get { - lock (overrides) { return isEmptyInternal; } + using (new ReadLock(rwl)) { return isEmptyInternal; } } } public bool Contains(T prefab) { - lock (overrides) { return ContainsInternal(prefab); } + using (new ReadLock(rwl)) { return ContainsInternal(prefab); } } public bool IsOverride(T prefab) { - lock (overrides) { return IsOverrideInternal(prefab); } + using (new ReadLock(rwl)) { return IsOverrideInternal(prefab); } } @@ -73,7 +96,7 @@ namespace Barotrauma private T? basePrefabInternal; private readonly List overrides = new List(); - private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal; + private T? activePrefabInternal => overrides.Count > 0 ? overrides.First() : basePrefabInternal; private void AddInternal(T prefab, bool isOverride) { @@ -84,7 +107,7 @@ namespace Barotrauma } else { - if (BasePrefab != null) + if (basePrefabInternal != null) { string prefabName = prefab is MapEntityPrefab mapEntityPrefab @@ -92,7 +115,7 @@ namespace Barotrauma : $"\"{prefab.Identifier}\""; throw new InvalidOperationException( $"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): " - + $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); + + $"a prefab with the same identifier from \"{activePrefabInternal!.ContentPackage?.Name ?? "[NULL]"}\" ({activePrefabInternal!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); } basePrefabInternal = prefab; } @@ -114,31 +137,12 @@ namespace Barotrauma SortInternal(); } - private void RemoveByFileInternal(ContentFile file, Action? callback) - { - for (int i = overrides.Count-1; i >= 0; i--) - { - var prefab = overrides[i]; - if (prefab.ContentFile == file) - { - RemoveInternal(prefab); - callback?.Invoke(prefab); - } - } - - if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) - { - RemoveInternal(basePrefabInternal); - callback?.Invoke(p); - } - } - private void SortInternal() { overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue)); } - private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any(); + private bool isEmptyInternal => basePrefabInternal is null && overrides.Count == 0; private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab); @@ -153,7 +157,7 @@ namespace Barotrauma { T? basePrefab; ImmutableArray overrideClone; - lock (overrides) + using (new ReadLock(rwl)) { basePrefab = basePrefabInternal; overrideClone = overrides.ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index c2a73e504..16e5022f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -315,6 +315,25 @@ namespace Barotrauma return floatValue; } + public static bool TryGetAttributeInt(this XElement element, string name, out int result) + { + var attribute = element?.GetAttribute(name); + result = default; + if (attribute == null) { return false; } + + if (int.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var intVal)) + { + result = intVal; + return true; + } + if (float.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal)) + { + result = (int)floatVal; + return true; + } + return false; + } + public static int GetAttributeInt(this XElement element, string name, int defaultValue) { var attribute = element?.GetAttribute(name); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 4ba71d446..e1ec4e598 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -353,7 +353,7 @@ namespace Barotrauma // Check for duplicate binds when introducing new binds foreach (var defaultBinding in defaultBindings) { - if (!savedBindings.ContainsKey(defaultBinding.Key)) + if (!IsSetToNone(defaultBinding.Value) && !savedBindings.ContainsKey(defaultBinding.Key)) { foreach (var savedBinding in savedBindings) { @@ -373,6 +373,8 @@ namespace Barotrauma } } } + + static bool IsSetToNone(KeyOrMouse keyOrMouse) => keyOrMouse == Keys.None && keyOrMouse == MouseButton.None; } // Clear the old chat binds for configs saved before the introduction of the new chat binds diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index ebd8a0b36..94e6c7bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -119,8 +119,6 @@ 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/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 3c274b2c3..b4aa8a883 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -3,6 +3,7 @@ using Barotrauma.IO; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; @@ -20,6 +21,16 @@ namespace Barotrauma.Steam public const string PreviewImageName = "PreviewImage.png"; public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; + public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId) + { + workshopId = null; + if (!contentPackage.UgcId.TryUnwrap(out var ugcId)) { return false; } + if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return false; } + + workshopId = steamWorkshopId; + return true; + } + public static partial class Workshop { private struct ItemEqualityComparer : IEqualityComparer @@ -110,7 +121,10 @@ namespace Barotrauma.Steam { NukeDownload(workshopItem); var toUninstall - = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + = ContentPackageManager.WorkshopPackages.Where(p => + p.UgcId.TryUnwrap(out var ugcId) + && ugcId is SteamWorkshopId { Value: var itemId } + && itemId == workshopItem.Id) .ToHashSet(); ContentPackageManager.EnabledPackages.DisableMods(toUninstall); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 823d7fd28..78cc321f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; using System.Globalization; +using System.Text.Unicode; namespace Barotrauma { @@ -30,18 +31,22 @@ namespace Barotrauma public static int LanguageVersion { get; private set; } = 0; - private readonly static Regex isCJK = new Regex( - @"\p{IsHangulJamo}|" + - @"\p{IsHiragana}|" + - @"\p{IsKatakana}|" + - @"\p{IsCJKRadicalsSupplement}|" + - @"\p{IsCJKSymbolsandPunctuation}|" + - @"\p{IsEnclosedCJKLettersandMonths}|" + - @"\p{IsCJKCompatibility}|" + - @"\p{IsCJKUnifiedIdeographsExtensionA}|" + - @"\p{IsCJKUnifiedIdeographs}|" + - @"\p{IsHangulSyllables}|" + - @"\p{IsCJKCompatibilityForms}"); + private static readonly ImmutableArray> CjkRanges = new[] + { + UnicodeRanges.HangulJamo, + UnicodeRanges.Hiragana, + UnicodeRanges.Katakana, + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.EnclosedCjkLettersandMonths, + UnicodeRanges.CjkCompatibility, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.HangulSyllables, + UnicodeRanges.CjkCompatibilityForms + }.Select(r => new Range(r.FirstCodePoint, r.FirstCodePoint+r.Length-1)) + .OrderBy(r => r.Start) + .ToImmutableArray(); /// /// Does the string contain symbols from Chinese, Japanese or Korean languages @@ -54,7 +59,24 @@ namespace Barotrauma public static bool IsCJK(string text) { if (string.IsNullOrEmpty(text)) { return false; } - return isCJK.IsMatch(text); + + for (int i = 0; i < text.Length; i++) + { + char chr = text[i]; + for (int j = 0; j < CjkRanges.Length; j++) + { + var range = CjkRanges[j]; + + // If chr < range.Start, we know that it can't + // be in any of the following ranges, so let's + // not even bother checking them + if (chr < range.Start) { break; } + + // This character is in a range, return true + if (range.Contains(chr)) { return true; } + } + } + return false; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 9ca176167..f66cdcfe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -50,10 +50,7 @@ namespace Barotrauma if (level > maxLevel) { maxLevel = level; } int price = BasePrice; - for (int i = 1; i <= level; i++) - { - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, i / (float)maxLevel) / 100); - } + price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); return location?.GetAdjustedMechanicalCost(price) ?? price; } } @@ -331,8 +328,6 @@ namespace Barotrauma public ContentXElement SourceElement { get; } - private bool disposed; - public bool SuppressWarnings { get; } public bool HideInMenus { get; } @@ -525,18 +520,12 @@ namespace Barotrauma public override void Dispose() { - if (!disposed) - { - Prefabs.Remove(this); #if CLIENT - Sprite?.Remove(); - Sprite = null; - DecorativeSprites.ForEach(sprite => sprite.Remove()); - targetProperties.Clear(); + Sprite?.Remove(); + Sprite = null; + DecorativeSprites.ForEach(sprite => sprite.Remove()); + targetProperties.Clear(); #endif - } - - disposed = true; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 30a219a32..3d19c61ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -16,18 +16,18 @@ namespace Barotrauma public bool IsNone() => this is None; public bool IsSome() => this is Some; - public bool TryUnwrap(out T outValue) + public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); + + public bool TryUnwrap(out T1 outValue) where T1 : T { switch (this) { - case Some { Value: var value }: + case Some { Value: T1 value }: outValue = value; return true; - case None _: + default: outValue = default!; return false; - default: - throw new ArgumentOutOfRangeException(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index e02fe1c68..f65b53aab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -23,16 +23,16 @@ namespace Barotrauma return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); } - public static Option ParseDerived(string str) + public static Option ParseDerived(TInput input) where TInput : notnull { - static Option none() => Option.None(); + static Option none() => Option.None(); - var derivedTypes = GetDerivedNonAbstract(); + var derivedTypes = GetDerivedNonAbstract(); - Option parseOfType(Type t) + Option parseOfType(Type t) { - //every T1 type is expected to have a method with the following signature: - // public static Option Parse(string str) + //every TBase type is expected to have a method with the following signature: + // public static Option Parse(TInput str) var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static); if (parseFunc is null) { return none(); } @@ -44,14 +44,17 @@ namespace Barotrauma 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); + //some hacky business to convert from Option to Option when we only know T2 at runtime + static Option convert(Option option) where T2 : TBase + => option.Select(v => (TBase)v); + Func, Option> f = convert; + var genericArgs = f.Method.GetGenericArguments(); + genericArgs[^1] = t; + var constructedConverter = + f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); - return constructedConverter.Invoke(null, new object?[] { parseFunc.Invoke(null, new object[] { str }) }) - as Option ?? none(); + return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) + as Option ?? none(); } return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs new file mode 100644 index 000000000..25a2ac7fa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs @@ -0,0 +1,34 @@ +using System.Threading; + +namespace Barotrauma.Threading +{ + internal readonly ref struct ReadLock + { + private readonly ReaderWriterLockSlim rwl; + public ReadLock(ReaderWriterLockSlim rwl) + { + this.rwl = rwl; + rwl.EnterReadLock(); + } + + public void Dispose() + { + rwl.ExitReadLock(); + } + } + + internal readonly ref struct WriteLock + { + private readonly ReaderWriterLockSlim rwl; + public WriteLock(ReaderWriterLockSlim rwl) + { + this.rwl = rwl; + rwl.EnterWriteLock(); + } + + public void Dispose() + { + rwl.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 1e642870f..6dd03983d 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,87 @@ +--------------------------------------------------------------------------------------------------------- +v0.19.5.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Progress on the new tutorials, the Roles tutorials in particular. Still a work in progress, but feedback is again more than welcome! +- Cleaned up networking and server list code to make them less error-prone and easier to work with in the future. Should not cause any functional differences, but please let us know if you notice any issues or oddities! +- Fixes and improvements to Camel. +- Fixed skill texts not being colored according to the user's skills in the repair UI. +- Fixed outpost service NPCs' titles (which are also used as the tooltips on the icons) not showing up in the multiplayer campaign. +- Further fixes to dragging animations. +- Fixed warning about duplicate keybinds being displayed for things set to None. +- Fixed clients who've gotten vote kicked getting vote kicked again when they join. +- Fixed favorited localhost servers causing a crash. +- Fixed respawn shuttle setting toggling on and off when adjusting other settings. +- Fixed skills increasing on death if the skill level is under the maximum initial skill. +- Fixes duplicate Captain Hognoses occasionally appearing in the crew list when selecting the Hognose mission in the multiplayer campaign. +- Fixed wrecked coilgun and railgun launch impulses. +- Fixed delayed status effects not working after the parent entity has been removed (breaking many medical items, e.g. ethanol). +- Fixed inability to buy anything from other stores when you've bought something from one of the stores in an outpost in the multiplayer campaign. + +Changes: +- Made crates deconstruct much faster to make them easier to get rid of. +- Sonar disruptions hide minerals. +- Gray out RangedWeapon's crosshair when reloading (similar to turret crosshairs). + +Fixes: +- Fixed clients downloading submarines they already have from the server if the mods those submarines are in are not currently enabled. +- Fixed submarines always saving in the root folder of a local mod, instead of the subfolder they were originally in. +- Fixed Reaper's Tax not stacking. +- Fixed turrets linked to the same loader messing up the upgrade store UI and causing item swaps to cost more than they should. +- Fixed status monitor calculating linked hulls' water amounts incorrectly (displaying the average of their water percentages, which isn't correct if the hulls aren't the same size). +- Fixes to messed up ruin decals in a bunch of ruin modules. +- Fixed a waypoint issue in the Alien_Entrance3 ruin module. +- Removed oxygen tanks from DockingModule_01_Colony. +- Tweaked the hulls and waypoints around Herja's top docking hatch to make it easier for bots to reach and weld. +- Fixed a waypoint/hull issue in Typhon's stowage compartment (waypoint in such a tight space the bots couldn't reach it). +- Fixed inactive components (components not currently sending any signal) not reactivating if their output is set to a non-empty value. +- Fixed duct block's misaligned broken sprite. + +Submarine editor: +- Fixed door gaps not appearing in the sub editor until you select the door + +Modding: +- Added CheckTalentAction, which can be used in events to check whether a target has unlocked a specific talent. +- Fixed ExtraLoad working the wrong way around on PowerTransfer components that generate/consume power (the extra load would supply power to the grid). Does not affect the vanilla game, because neither junction boxes or relays generate or consume power. + +--------------------------------------------------------------------------------------------------------- +v0.19.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed networking issues that prevented joining SteamP2P servers. +- Fixed crashing on startup if GameAnalytics is enabled. +- Fixed sub editor background images not saving if you leave the editor and quit the game without exiting the image editing mode first. +- Camel hull fixes. +- Fixed a draw order issue in unit load device. +- Fixed unit load device's inventory slot layout. +- Fixed submarine tier set in the sub editor not being saved, causing all subs to use the default tier determined by the price. + +Changes: +- Made extra sales from "traveling tradesman" talent stack. + +Fixes: +- Made bots better at figuring out which button controls a door when there's some complex circuit involved. Previously the bots would try to find a button connected to any of the door's connections via wires/circuits, now only the toggle and set_state inputs are considered. +- Bots now heavily prefer using buttons linked to the door in the sub editor. Can be used as another way to help the bots figure out which button they should press in situations with multiple buttons and complex door control logic. +- Fixed bots failing to find a path to a couple of spots in Herja. +- Fixed alien materials (physicorium, incendium, fulgurium, dementonite, paralyxis) not being shown on the mineral scanner. +- Another fix to cave generation to prevent it from creating impassable paths. +- Fixed inability to use manual assignment for bot orders with options. +- Fixed all boolean components (And, Or, Xor) using the And Component's tooltip for the "timeframe" property. +- Fixed boolean operator component (And, Or, Xor) timeframes not working correctly in some situations (non-zero timeframe, empty false output). The component would deactivate as soon as it stops sending an output, which could prevent some inputs from timing out (meaning that the component could send a signal again as soon as it receives signal A, even if signal B hasn't been received within the timeframe). +- Fixed PUCS consuming the medical item inside it when a welding fuel or incendium tank is inserted. + +Multiplayer: +- Fixed inventory and wallet resetting if a campaign round ends when a client's character has spawned, but the client is not currently controlling it (e.g. due to getting kicked to the lobby). +- Fixed spectator checkbox overlapping with the character info if you get kicked to the lobby mid-round. + +Modding: +- Fixed items/structures now falling back to the description defined in the xml even if it's empty, if the description is not defined for the selected language (instead of using English instead). +- Fixed using the "reloadwearables" and "loadwearable" console commands outside the character editor crashing the game. +- Fixed character editor crash if you first reload textures and then recreate the ragdoll. +- Changed how submarine upgrades are calculated, now no longer adds previous levels' costs to the price, but rather relies on higher increasehigh values + --------------------------------------------------------------------------------------------------------- v0.19.3.0 --------------------------------------------------------------------------------------------------------- @@ -44,7 +128,6 @@ Fixes: - Fixed chaingun rotation speed not being affected by the weapons skill. - Fixed crashing when using ':' in item assembly names on Linux platforms. - Fixed ImmuneToPressure ability flag being ignored on characters who don't need air (in practice meaning that you can get killed by pressure if you get huskified even if you have a talent that makes you immune to pressure). -- Disallow using the "reloadwearables" and "loadwearable" when a gamesession is active because it leads to a crash. These commands are intended to be used in the character editor. - Fixed geneticmaterialcrawler_unresearched3 producing mudraptor genes. Submarine editor: diff --git a/Libraries/Facepunch.Steamworks/ServerList/Base.cs b/Libraries/Facepunch.Steamworks/ServerList/Base.cs index 9a6868702..9a5cb1125 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Base.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Base.cs @@ -23,17 +23,17 @@ namespace Steamworks.ServerList /// /// When a new server is added, this function will get called /// - public event Action OnChanges; + public Action OnChanges; /// /// Called for every responsive server /// - public event Action OnResponsiveServer; + public Action OnResponsiveServer; /// /// Called for every unresponsive server /// - public event Action OnUnresponsiveServer; + public Action OnUnresponsiveServer; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -65,14 +65,14 @@ namespace Steamworks.ServerList var thisRequest = request; - while ( IsRefreshing ) + while ( true ) { await Task.Delay( 33 ); // // The request has been cancelled or changed in some way // - if ( request.Value == IntPtr.Zero || thisRequest.Value != request.Value ) + if ( request.Value == IntPtr.Zero || thisRequest.Value != request.Value || !IsRefreshing ) return false; if ( !SteamClient.IsValid ) diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 0561b8dd5..510227bbf 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -114,9 +114,9 @@ namespace Steamworks public struct FriendGameInfo { - internal ulong GameID; // m_gameID class CGameID - internal uint GameIP; // m_unGameIP uint32 - internal ulong SteamIDLobby; // m_steamIDLobby class CSteamID + public ulong GameID; // m_gameID class CGameID + public uint GameIP; // m_unGameIP uint32 + public ulong SteamIDLobby; // m_steamIDLobby class CSteamID public int ConnectionPort; public int QueryPort; diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 1a3b3fb2d..2b0fdb57a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -153,13 +153,13 @@ namespace Steamworks.Data } /// - /// Refreshes metadata for a lobby you're not necessarily in right now - /// you never do this for lobbies you're a member of, only if your - /// this will send down all the metadata associated with a lobby - /// this is an asynchronous call - /// returns false if the local user is not connected to the Steam servers - /// results will be returned by a LobbyDataUpdate_t callback - /// if the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false + /// Refreshes metadata for a lobby you're not necessarily in right now. + /// You never do this for lobbies you're a member of, only if your + /// this will send down all the metadata associated with a lobby. + /// This is an asynchronous call. + /// Returns false if the local user is not connected to the Steam servers. + /// Results will be returned by a LobbyDataUpdate_t callback. + /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false. /// public bool Refresh() {