diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 4be093613..3c2985c11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -10,7 +10,7 @@ namespace Barotrauma public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (Character == Character.Controlled) { return; } - if (!debugai) { return; } + if (!DebugAI) { return; } Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index cf8c7c628..44032309e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -19,6 +19,17 @@ namespace Barotrauma private IEnumerable FadeOutColors(float time) { + + Dictionary originalColors = new Dictionary(); + foreach (var item in thalamusItems) + { + originalColors.Add(item, item.SpriteColor); + } + foreach (var structure in thalamusStructures) + { + originalColors.Add(structure, structure.SpriteColor); + } + float timer = 0; while (timer < time) { @@ -26,15 +37,16 @@ namespace Barotrauma float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); foreach (var item in thalamusItems) { + if (item.Color.A == 0) { continue; } if (item.Prefab.BrokenSprites.None()) { - Color c = item.Prefab.SpriteColor; + Color c = originalColors[item]; item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } } foreach (var structure in thalamusStructures) { - Color c = structure.Prefab.SpriteColor; + Color c = originalColors[structure]; structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index a350ef029..8f503a225 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -6,11 +6,17 @@ namespace Barotrauma { partial class Attack { - [Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] + [Serialize("StructureBlunt", IsPropertySaveable.Yes, description: "Name of the sound effect the attack makes when it hits a structure."), Editable()] public string StructureSoundType { get; private set; } + /// + /// Sound to play when the attack deals damage. + /// private RoundSound sound; + /// + /// Particle emitter to use when the attack deals damage. + /// private ParticleEmitter particleEmitter; partial void InitProjSpecific(ContentXElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0f86bcc9c..5c54b9ba2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -582,7 +582,7 @@ namespace Barotrauma float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); foreach (MapEntity entity in entityList) { - if (!(entity is Item item)) + if (entity is not Item item) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 090a8e40d..ada25782a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -153,6 +153,7 @@ namespace Barotrauma private static readonly List bossProgressBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None; private static GUILayoutGroup bossHealthContainer; @@ -202,10 +203,15 @@ namespace Barotrauma public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) { + if (cachedHudTextLanguage != GameSettings.CurrentConfig.Language) + { + cachedHudTexts.Clear(); + } Identifier key = (textTag + keyBind).ToIdentifier(); if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; cachedHudTexts.Add(key, text); + cachedHudTextLanguage = GameSettings.CurrentConfig.Language; return text; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 57a4c8015..107816307 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -701,10 +701,11 @@ namespace Barotrauma blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + float afflictionGrainStrength = affliction.GetScreenGrainStrength(); if (afflictionGrainStrength > 0.0f) { - grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); + grainStrength = Math.Max(grainStrength, afflictionGrainStrength); Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } @@ -1020,12 +1021,8 @@ namespace Barotrauma foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - if (affliction.Prefab.AfflictionOverlay != null) - { - Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); - } + affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); } float damageOverlayAlpha = DamageOverlayTimer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 76afc54f2..1e40e293b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -125,9 +125,11 @@ namespace Barotrauma public static string IncrementModVersion(string modVersion) { + if (string.IsNullOrWhiteSpace(modVersion)) { return string.Empty; } + //look for an integer at the end of the string and increment it int startIndex = modVersion.Length - 1; - while (char.IsDigit(modVersion[startIndex])) { startIndex--; } + while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; } startIndex++; if (startIndex >= modVersion.Length diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 728d7233b..e3ea672f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -425,6 +425,10 @@ namespace Barotrauma { CheatsEnabled = true; SteamAchievementManager.CheatsEnabled = true; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + campaign.CheatsEnabled = true; + } NewMessage("Enabled cheat commands.", Color.Red); #if USE_STEAM NewMessage("Steam achievements have been disabled during this play session.", Color.Red); @@ -632,15 +636,29 @@ namespace Barotrauma commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) => { if (Character.Controlled == null) { return; } - WikiImage.Create(Character.Controlled); + try + { + WikiImage.Create(Character.Controlled); + } + catch (Exception e) + { + DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e); + } })); commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) => { if (Submarine.MainSub == null) { return; } - MapEntity.SelectedList.Clear(); - MapEntity.ClearHighlightedEntities(); - WikiImage.Create(Submarine.MainSub); + try + { + MapEntity.SelectedList.Clear(); + MapEntity.ClearHighlightedEntities(); + WikiImage.Create(Submarine.MainSub); + } + catch (Exception e) + { + DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e); + } })); AssignRelayToServer("kick", false); @@ -1146,7 +1164,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); + NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("devmode", false); @@ -1248,8 +1266,8 @@ namespace Barotrauma AssignOnExecute("debugai", (string[] args) => { - HumanAIController.debugai = !HumanAIController.debugai; - if (HumanAIController.debugai) + HumanAIController.DebugAI = !HumanAIController.DebugAI; + if (HumanAIController.DebugAI) { GameMain.DevMode = true; GameMain.DebugDraw = true; @@ -1264,7 +1282,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); + NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); @@ -2323,7 +2341,7 @@ namespace Barotrauma { if (mapEntity is Item item) { - item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, + item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); } @@ -2859,7 +2877,7 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) => { @@ -3345,6 +3363,11 @@ namespace Barotrauma else { NewMessage("Level seed: " + Level.Loaded.Seed); + NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); + NewMessage("Mirrored: " + Level.Loaded.Mirrored); + NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); + NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 1aa23640c..6930395a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -517,6 +517,10 @@ namespace Barotrauma GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) { + if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count) + { + throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}"); + } Texture2D tex = textures[gd.TexIndex]; Vector2 drawOffset; drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 81598fdf2..e19738609 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -240,7 +240,7 @@ namespace Barotrauma private void UpdatePending() { - if (!(pendingHealList is { } healList)) { return; } + if (pendingHealList is not { } healList) { return; } ImmutableArray pendingList = medicalClinic.PendingHeals.ToImmutableArray(); @@ -493,20 +493,26 @@ namespace Barotrauma GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) { - OnClicked = (_, _) => + OnClicked = (button, _) => { + if (isWaitingForServer) { return true; } + + button.Enabled = false; isWaitingForServer = true; - medicalClinic.TreatAllButtonAction(OnReceived); + + bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton()); + if (!wasSuccessful) { ReEnableButton(); } + + void ReEnableButton() + { + isWaitingForServer = false; + button.Enabled = true; + } return true; } }; crewHealList = new CrewHealList(crewList, parent, treatAllButton); - - void OnReceived(MedicalClinic.CallbackOnlyRequest obj) - { - isWaitingForServer = false; - } } private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) @@ -524,7 +530,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { - TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)(info.Character?.HealthPercentage ?? 100f)}"), + TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(info.Character?.HealthPercentage ?? 100f)}"), TextColor = GUIStyle.Green }; @@ -585,8 +591,10 @@ namespace Barotrauma OnClicked = (button, _) => { button.Enabled = false; - medicalClinic.HealAllButtonAction(request => + isWaitingForServer = true; + bool wasSuccessful = medicalClinic.HealAllButtonAction(request => { + isWaitingForServer = false; switch (request.HealResult) { case MedicalClinic.HealRequestResult.InsufficientFunds: @@ -600,6 +608,12 @@ namespace Barotrauma button.Enabled = true; ClosePopup(); }); + + if (!wasSuccessful) + { + isWaitingForServer = false; + button.Enabled = true; + } ClosePopup(); return true; } @@ -610,11 +624,19 @@ namespace Barotrauma ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { + if (isWaitingForServer) { return true; } + button.Enabled = false; - medicalClinic.ClearAllButtonAction(_ => + isWaitingForServer = true; + + bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton()); + if (!wasSuccessful) { ReEnableButton(); } + + void ReEnableButton() { + isWaitingForServer = false; button.Enabled = true; - }); + } return true; } }; @@ -701,10 +723,15 @@ namespace Barotrauma OnClicked = (button, _) => { button.Enabled = false; - medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => + bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => { button.Enabled = true; }); + + if (!wasSuccessful) + { + button.Enabled = true; + } return true; } }; @@ -792,7 +819,13 @@ namespace Barotrauma selectedCrewAfflictionList = popupAfflictionList; isWaitingForServer = true; - medicalClinic.RequestAfflictions(info, OnReceived); + bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived); + + if (!wasSuccessful) + { + isWaitingForServer = false; + ClosePopup(); + } void OnReceived(MedicalClinic.AfflictionRequest request) { @@ -800,6 +833,16 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { + switch (request.Result) + { + case MedicalClinic.RequestResult.CharacterInfoMissing: + DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName}\" in medical clini because the character health was missing."); + break; + case MedicalClinic.RequestResult.CharacterNotFound: + DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName} in medical clinic because the server was unable to find a character with ID {info.ID}."); + break; + } + feedbackBlock.Text = GetErrorText(request.Result); feedbackBlock.TextColor = GUIStyle.Red; return; @@ -953,14 +996,20 @@ namespace Barotrauma } existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); + ToggleElements(ElementState.Disabled, elementsToDisable); - medicalClinic.AddPendingButtonAction(existingMember, request => + bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request => { if (request.Result == MedicalClinic.RequestResult.Timeout) { ToggleElements(ElementState.Enabled, elementsToDisable); } }); + + if (!wasSuccessful) + { + ToggleElements(ElementState.Enabled, elementsToDisable); + } } #warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString @@ -1090,9 +1139,8 @@ namespace Barotrauma { return result switch { - MedicalClinic.RequestResult.Error => TextManager.Get("error"), MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), - _ => "What the hell did you just do" // this should never happen + _ => TextManager.Get("error") }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 4ec0e6505..0df30ed06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1072,18 +1072,19 @@ namespace Barotrauma if (save) { GUI.SetSavingIndicatorState(true); - if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); } - - // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + if (GameSession.Campaign is CampaignMode campaign) { - spCampaign.UpdateStoreStock(); + if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + { + spCampaign.UpdateStoreStock(); + } + GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + campaign.End(); } - SaveUtil.SaveGame(GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 61feab4eb..e8823f5dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -148,7 +148,10 @@ namespace Barotrauma } if (Submarine.MainSub == null || Level.Loaded == null) { return; } - endRoundButton.Visible = false; + bool allowEndingRound = false; + endRoundButton.Color = endRoundButton.Style.Color; + endRoundButton.HoverColor = endRoundButton.Style.HoverColor; + RichString overrideEndRoundButtonToolTip = string.Empty; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); LocalizedString buttonText = ""; switch (availableTransition) @@ -159,12 +162,12 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousEmptyLocation: @@ -172,32 +175,37 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } - break; case TransitionType.None: default: - if (Level.Loaded.Type == LevelData.LevelType.Outpost && - !Level.Loaded.IsEndBiome && - (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) + bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true }; + if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome && + (inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { + if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub)) + { + overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub"); + endRoundButton.Color = GUIStyle.Red * 0.7f; + endRoundButton.HoverColor = GUIStyle.Red; + } buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } else { - endRoundButton.Visible = false; + allowEndingRound = false; } break; } if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) { - endRoundButton.Visible = false; + allowEndingRound = false; } + if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; } - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } - + endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) { if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) @@ -215,7 +223,11 @@ namespace Barotrauma prevCampaignUIAutoOpenType = availableTransition; } endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); - if (endRoundButton.Text != buttonText) + if (overrideEndRoundButtonToolTip != string.Empty) + { + endRoundButton.ToolTip = overrideEndRoundButtonToolTip; + } + else if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; } @@ -328,6 +340,11 @@ namespace Barotrauma CampaignUI.UpgradeStore?.RequestRefresh(); break; } + + if (npc.AIController is HumanAIController humanAi && humanAi.IsInHostileFaction()) + { + npc.Speak(TextManager.Get("dialoglowrepcampaigninteraction").Value, identifier: "dialoglowrepcampaigninteraction".ToIdentifier(), minDurationBetweenSimilar: 60.0f); + } } public override void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 53cf2e86b..4310ee700 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -348,7 +348,7 @@ namespace Barotrauma //-------------------------------------- //wait for the new level to be loaded - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60); + DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut; while (Level.Loaded == prevLevel || Level.Loaded == null) { if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } @@ -358,8 +358,12 @@ namespace Barotrauma endTransition.Stop(); overlayColor = Color.Transparent; - if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } - if (!(Screen.Selected is RoundSummaryScreen)) + if (DateTime.Now > timeOut) + { + DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish."); + GameMain.NetLobbyScreen.Select(); + } + if (Screen.Selected is not RoundSummaryScreen) { if (continueButton != null) { @@ -947,7 +951,9 @@ namespace Barotrauma if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } - if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null && + /*can't apply until we have the latest save file*/ + !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 3b4d31cf6..796546c78 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -17,7 +17,8 @@ namespace Barotrauma { Undecided, Success, - Error, + CharacterInfoMissing, + CharacterNotFound, Timeout } @@ -34,7 +35,9 @@ namespace Barotrauma private readonly List> addRequests = new List>(); private readonly List> removeRequests = new List>(); - public void RequestAfflictions(CharacterInfo info, Action onReceived) + private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10); + + public bool RequestAfflictions(CharacterInfo info, Action onReceived) { if (GameMain.IsSingleplayer) { @@ -42,23 +45,26 @@ namespace Barotrauma if (Screen.Selected is TestScreen) { onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); - return; + return true; } #endif if (info is not { Character.CharacterHealth: { } health }) { - onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); - return; + onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray.Empty)); + return true; } - ImmutableArray pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); + ImmutableArray pendingAfflictions = GetAllAfflictions(health); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); - return; + return true; } - afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); - SendAfflictionRequest(info); + return requestBucket.TryEnqueue(() => + { + afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendAfflictionRequest(info); + }); } public void RequestLatestPending(Action onReceived) @@ -66,8 +72,11 @@ namespace Barotrauma // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI if (GameMain.IsSingleplayer) { return; } - pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); - SendPendingRequest(); + requestBucket.TryEnqueue(() => + { + pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendPendingRequest(); + }); } public void Update(float deltaTime) @@ -79,6 +88,7 @@ namespace Barotrauma UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); + requestBucket.Update(deltaTime); static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } @@ -146,21 +156,25 @@ namespace Barotrauma return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } - public void TreatAllButtonAction(Action onReceived) + public bool TreatAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { AddEverythingToPending(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - addRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + }); } - public void HealAllButtonAction(Action onReceived) + + public bool HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { @@ -171,33 +185,39 @@ namespace Barotrauma OnUpdate?.Invoke(); } - return; + return true; } - if (campaign?.CampaignUI?.MedicalClinic is { } ui) + if (campaign?.CampaignUI?.MedicalClinic is { } openedUi) { - ui.ClosePopup(); + openedUi.ClosePopup(); } - healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + }); } - public void ClearAllButtonAction(Action onReceived) + public bool ClearAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { ClearPendingHeals(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + }); } - private void ClearRequstReceived() + private void ClearRequestReceived() { ClearPendingHeals(); if (TryDequeue(clearAllRequests, out var callback)) @@ -224,28 +244,31 @@ namespace Barotrauma OnUpdate?.Invoke(); } - public void AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) + public bool AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) { if (GameMain.IsSingleplayer) { InsertPendingCrewMember(crewMember); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - addRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + }); } - public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) + public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) { if (GameMain.IsSingleplayer) { RemovePendingAffliction(crewMember, affliction); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } INetSerializableStruct removedAffliction = new NetRemovedAffliction @@ -254,11 +277,14 @@ namespace Barotrauma Affliction = affliction }; - removeRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + removeRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + }); } - private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) + private void NewAdditionReceived(IReadMessage inc, MessageFlag flag) { var crewMembers = INetSerializableStruct.Read>(inc); foreach (var crewMember in crewMembers) @@ -300,7 +326,7 @@ namespace Barotrauma NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { - RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; + RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } @@ -336,7 +362,7 @@ namespace Barotrauma IWriteMessage msg = StartSending(); msg.WriteByte((byte)header); netStruct?.Write(msg); - GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); + GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod); } public void ClientRead(IReadMessage inc) @@ -356,7 +382,7 @@ namespace Barotrauma PendingRequestReceived(inc); break; case NetworkHeader.ADD_PENDING: - NewAdditonReceived(inc, flag); + NewAdditionReceived(inc, flag); break; case NetworkHeader.REMOVE_PENDING: NewRemovalReceived(inc, flag); @@ -365,7 +391,7 @@ namespace Barotrauma HealRequestReceived(inc); break; case NetworkHeader.CLEAR_PENDING: - ClearRequstReceived(); + ClearRequestReceived(); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index cabf5bcbb..8ba436270 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -40,7 +40,7 @@ namespace Barotrauma private void CreateMessageBox(string author) { - Vector2 relativeSize = new Vector2(0.3f * GUI.AspectRatioAdjustment, 0.15f); + Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 1f55a4ff8..53bcf4f1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -213,10 +213,11 @@ namespace Barotrauma }; List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); - if (!selectedMissions.Any() && startLocation != null) + if (startLocation != null) { foreach (Mission mission in startLocation.SelectedMissions) { + if (missionsToDisplay.Contains(mission)) { continue; } if (!mission.Prefab.ShowInMenus) { continue; } if (mission.Locations[0] == mission.Locations[1] || mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) @@ -316,7 +317,7 @@ namespace Barotrauma RichString reputationText = displayedMission.GetReputationRewardText(); if (!reputationText.IsNullOrEmpty()) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true); } int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index f50239f35..428996d21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -88,6 +88,11 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { + UInt16 userID = msg.ReadUInt16(); + if (userID != Entity.NullEntityID) + { + user = Entity.FindEntityByID(userID) as Character; + } CurrPowerConsumption = powerConsumption; charging = true; timer = Duration; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index b29eb3176..763d89b4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -21,6 +21,9 @@ namespace Barotrauma.Items.Components /// private float lightColorMultiplier; + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the light sprite.")] + public float LightSpriteScale { get; set; } + public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -92,7 +95,13 @@ namespace Barotrauma.Items.Components { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } - Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + Light.LightSprite.Draw(spriteBatch, + new Vector2(drawPos.X, -drawPos.Y), + color * lightBrightness, + origin, + -Light.Rotation, + item.Scale * LightSpriteScale, + Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 8e30123bf..49379a2f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -528,7 +528,7 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); + var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct(); LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -550,6 +550,8 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } + + toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; if (!requiredItemPrefab.Description.IsNullOrEmpty()) { toolTipText += '\n' + requiredItemPrefab.Description; @@ -594,7 +596,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); + GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement); tooltip = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index abe3fd223..82dbf9a7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -403,7 +403,8 @@ namespace Barotrauma.Items.Components private bool VisibleOnItemFinder(Item it) { - if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } + if (it?.Submarine == null) { return false; } + if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } if (it.NonInteractable || it.HiddenInGame) { return false; } if (it.GetComponent() == null) { return false; } @@ -702,6 +703,12 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { + if (miniMapFrame == null) + { + //frame not created yet, could happen if the item hasn't been inside any sub this round? + return; + } + if (Voltage < MinVoltage) { Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); @@ -1057,7 +1064,9 @@ namespace Barotrauma.Items.Components waterVolume += linkedHull.WaterVolume; totalVolume += linkedHull.Volume; } - hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100); + hullData.HullWaterAmount = + waterVolume > 1.0f ? + MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 628294d1d..3a780a5e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1311,7 +1311,6 @@ namespace Barotrauma.Items.Components float worldPingRadiusSqr = worldPingRadius * worldPingRadius; disruptedDirections.Clear(); - if (Level.Loaded == null) { return; } for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { @@ -1434,8 +1433,10 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(submarine)) { continue; } } - Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); - worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); + //display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above) + //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts + Rectangle worldBorders = submarine.GetDockedBorders(); + worldBorders.Location += submarine.WorldPosition.ToPoint(); if (Submarine.RectContains(worldBorders, pingSource)) { CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 6a2b6571b..aaca8fda0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); - SpreadCounter = msg.ReadByte(); + spreadIndex = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 29b3adae2..2f4c6a63c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components private readonly List uiElements = new List(); private GUILayoutGroup uiElementContainer; + private bool readingNetworkEvent; + private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); public override bool RecreateGUIOnResolutionChange => true; @@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -126,7 +128,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -161,7 +163,7 @@ namespace Barotrauma.Items.Components { TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -181,12 +183,12 @@ namespace Barotrauma.Items.Components }; btn.OnClicked += (_, userdata) => { - CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; + CustomInterfaceElement btnElement = userdata as CustomInterfaceElement; if (GameMain.Client == null) { ButtonClicked(btnElement); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this, new EventData(btnElement)); } @@ -248,7 +250,7 @@ namespace Barotrauma.Items.Components int visibleElementCount = 0; foreach (var uiElement in uiElements) { - if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } + if (uiElement.UserData is not CustomInterfaceElement element) { continue; } bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) @@ -297,9 +299,10 @@ namespace Barotrauma.Items.Components LocalizedString CreateLabelText(int elementIndex) { - return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? + var label = customInterfaceElementList[elementIndex].Label; + return string.IsNullOrWhiteSpace(label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : - customInterfaceElementList[elementIndex].Label; + TextManager.Get(label).Fallback(label); } uiElementContainer.Recalculate(); @@ -334,7 +337,9 @@ namespace Barotrauma.Items.Components { if (uiElements[i] is GUITextBox tb) { - tb.Text = customInterfaceElementList[i].Signal; + tb.Text = Screen.Selected is { IsEditor: true } ? + customInterfaceElementList[i].Signal : + TextManager.Get(customInterfaceElementList[i].Signal).Value; } else if (uiElements[i] is GUINumberInput ni) { @@ -386,45 +391,53 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { - for (int i = 0; i < customInterfaceElementList.Count; i++) + readingNetworkEvent = true; + try { - var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + for (int i = 0; i < customInterfaceElementList.Count; i++) { - string newValue = msg.ReadString(); - if (!element.IsNumberInput) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - TextChanged(element, newValue); + string newValue = msg.ReadString(); + if (!element.IsNumberInput) + { + TextChanged(element, newValue); + } + else + { + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(newValue, out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): + ValueChanged(element, value); + break; + } + } } else { - switch (element.NumberType) + bool elementState = msg.ReadBoolean(); + if (element.ContinuousSignal) { - case NumberType.Int when int.TryParse(newValue, out int value): - ValueChanged(element, value); - break; - case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): - ValueChanged(element, value); - break; + ((GUITickBox)uiElements[i]).Selected = elementState; + TickBoxToggled(element, elementState); + } + else if (elementState) + { + ButtonClicked(element); } } } - else - { - bool elementState = msg.ReadBoolean(); - if (element.ContinuousSignal) - { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(element, elementState); - } - else if (elementState) - { - ButtonClicked(element); - } - } - } - UpdateSignalsProjSpecific(); + UpdateSignalsProjSpecific(); + } + finally + { + readingNetworkEvent = false; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 9924b8ad8..426d2c127 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { - if (limb.Mass < 1.0f) { continue; } + if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; } float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f); float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f); Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 2ffbf71ec..bfc7c5eb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1398,7 +1398,7 @@ namespace Barotrauma } else { - throw new Exception("Failed to read component state - " + components[componentIndex].GetType() + " is not IServerSerializable."); + throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable."); } } break; @@ -1411,13 +1411,14 @@ namespace Barotrauma } else { - throw new Exception("Failed to read inventory state - " + components[containerIndex].GetType() + " is not an ItemContainer."); + throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer."); } } break; case EventType.Status: + bool loadingRound = msg.ReadBoolean(); float newCondition = msg.ReadSingle(); - SetCondition(newCondition, isNetworkEvent: true); + SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); @@ -1459,9 +1460,9 @@ namespace Barotrauma byte length = msg.ReadByte(); for (int i = 0; i < length; i++) { - var statIdentifier = INetSerializableStruct.Read(msg); + var statIdentifier = INetSerializableStruct.Read(msg); var statValue = msg.ReadSingle(); - StatManager.ApplyStat(statIdentifier, statValue); + StatManager.ApplyStatDirect(statIdentifier, statValue); } break; case EventType.Upgrade: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 77189d2d9..362dafa31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -320,5 +321,45 @@ namespace Barotrauma return IsHorizontal ? rect.Height : rect.Width; } } + + public override void UpdateEditing(Camera cam, float deltaTime) + { + if (editingHUD == null || editingHUD.UserData != this) + { + editingHUD = CreateEditingHUD(); + } + } + private GUIComponent CreateEditingHUD(bool inGame = false) + { + + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) + { + UserData = this + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = (int)(GUI.Scale * 5) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("entityname.gap"), font: GUIStyle.LargeFont); + var hiddenInGameTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), TextManager.Get("sp.hiddeningame.name")) + { + Selected = HiddenInGame + }; + hiddenInGameTickBox.OnSelected += (GUITickBox tickbox) => + { + HiddenInGame = tickbox.Selected; + return true; + }; + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y * 1.25f))); + + PositionEditingHUD(); + + return editingHUD; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 37b05c639..c8ec7bb9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -240,7 +240,8 @@ namespace Barotrauma.Lights range * ((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) * (light.CastShadows ? 10.0f : 1.0f) * - (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)); + (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)) * + light.PriorityMultiplier; } //find the lights with an active light volume diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 428d4e0b3..724214955 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -377,6 +377,8 @@ namespace Barotrauma.Lights public float Priority; + public float PriorityMultiplier = 1.0f; + private Vector2 lightTextureTargetSize; public Vector2 LightTextureTargetSize diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index e881ebf77..9bdf822ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -537,7 +537,7 @@ namespace Barotrauma float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; if (!invalidMessage && i < Sections.Length) { - SetDamage(i, damage); + SetDamage(i, damage, isNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 460d06df1..6a5bfb3ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -75,7 +75,7 @@ namespace Barotrauma.Networking float gain = 1.0f; float noiseGain = 0.0f; Vector3? position = null; - if (character != null) + if (character != null && !character.IsDead) { if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index d31ea903b..2e938e4a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -16,6 +15,10 @@ namespace Barotrauma.Networking { sealed class GameClient : NetworkMember { + public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100); + //this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file + public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150); + public override bool IsClient => true; public override bool IsServer => false; @@ -76,6 +79,8 @@ namespace Barotrauma.Networking Interrupted } + private UInt16? debugStartGameCampaignSaveID; + private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted; public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize; @@ -512,6 +517,7 @@ namespace Barotrauma.Networking DisplayInLoadingScreens = true }; Quit(); + GUI.DisableHUD = false; GameMain.ServerListScreen.Select(); return; } @@ -859,17 +865,24 @@ namespace Barotrauma.Networking ContentFile file = ContentPackageManager.EnabledPackages.All .Select(p => p.Files.FirstOrDefault(f => f.Path == filePath)) - .FirstOrDefault(f => !(f is null)); + .FirstOrDefault(f => f is not null); contentToPreload.AddIfNotNull(file); } + string campaignErrorInfo = string.Empty; + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + campaignErrorInfo = $" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}."; + } + GameMain.GameSession.EventManager.PreloadContent(contentToPreload); int subEqualityCheckValue = inc.ReadInt32(); if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0)) { - string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + - " There may have been an error in receiving the up-to-date submarine file from the server."; + string errorMsg = + "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " + + $"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -886,7 +899,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. Mission count doesn't match the server. " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -899,7 +912,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -922,7 +935,7 @@ namespace Barotrauma.Networking ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; + ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -1322,6 +1335,8 @@ namespace Barotrauma.Networking eventErrorWritten = false; GameMain.NetLobbyScreen.StopWaitingForStartRound(); + debugStartGameCampaignSaveID = null; + while (CoroutineManager.IsCoroutineRunning("EndGame")) { EndCinematic?.Stop(); @@ -1471,7 +1486,28 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } - else if (campaign.Map == null) + + if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) || + NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) + { + campaign.PendingSaveID = campaignSaveID; + DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut; + while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) + { + if (DateTime.Now > saveFileTimeOut) + { + GameStarted = true; + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout")); + GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; + //use success status, even though this is a failure (no need to show a console error because we show it in the message box) + yield return CoroutineStatus.Success; + } + yield return new WaitForSeconds(0.1f); + } + } + + if (campaign.Map == null) { GameStarted = true; DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); @@ -1480,30 +1516,14 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - if (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) - { - campaign.PendingSaveID = campaignSaveID; - DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0,0,60); - while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) - { - if (DateTime.Now > saveFileTimeOut) - { - 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; - yield return CoroutineStatus.Failure; - } - yield return new WaitForSeconds(0.1f); - } - } - campaign.Map.SelectLocation(selectedLocationIndex); LevelData levelData = nextLocationIndex > -1 ? campaign.Map.Locations[nextLocationIndex].LevelData : campaign.Map.Connections[nextConnectionIndex].LevelData; + debugStartGameCampaignSaveID = campaign.LastSaveID; + if (roundSummary != null) { loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null); @@ -1697,7 +1717,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); } + GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType); ServerSettings.ServerDetailsChanged = true; @@ -2587,31 +2607,24 @@ namespace Barotrauma.Networking public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { msg.WriteBoolean(characterInfo == null); + msg.WritePadBits(); if (characterInfo == null) { return; } - msg.WriteString(newName ?? string.Empty); + var head = characterInfo.Head; - msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count); - foreach (Identifier tag in characterInfo.Head.Preset.TagSet) - { - msg.WriteIdentifier(tag); - } - msg.WriteByte((byte)characterInfo.Head.HairIndex); - msg.WriteByte((byte)characterInfo.Head.BeardIndex); - msg.WriteByte((byte)characterInfo.Head.MoustacheIndex); - msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex); - msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); - msg.WriteColorR8G8B8(characterInfo.Head.HairColor); - msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); + var netInfo = new NetCharacterInfo( + NewName: newName ?? string.Empty, + Tags: head.Preset.TagSet.ToImmutableArray(), + HairIndex: (byte)head.HairIndex, + BeardIndex: (byte)head.BeardIndex, + MoustacheIndex: (byte)head.MoustacheIndex, + FaceAttachmentIndex: (byte)head.FaceAttachmentIndex, + SkinColor: head.SkinColor, + HairColor: head.HairColor, + FacialHairColor: head.FacialHairColor, + JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray()); - var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; - int count = Math.Min(jobPreferences.Count, 3); - msg.WriteByte((byte)count); - for (int i = 0; i < count; i++) - { - msg.WriteIdentifier(jobPreferences[i].Prefab.Identifier); - msg.WriteByte((byte)jobPreferences[i].Variant); - } + msg.WriteNetSerializableStruct(netInfo); } public void Vote(VoteType voteType, object data) @@ -2864,12 +2877,14 @@ namespace Barotrauma.Networking ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public bool SpectateClicked(GUIButton button, object userData) + public bool SpectateClicked(GUIButton button, object _) { - MultiPlayerCampaign campaign = + MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; - if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID) + + if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) || + (campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID))) { new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress")); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index c51b66457..3cff1e7b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -92,10 +92,10 @@ namespace Barotrauma.Networking Name = GameMain.Client.Name, OwnerKey = ownerKey, SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket switch + SteamAuthTicket = steamAuthTicket?.Data switch { null => Option.None(), - var ticket => Option.Some(ticket.Data) + var ticketData => Option.Some(ticketData) }, GameVersion = GameMain.Version.ToString(), Language = GameSettings.CurrentConfig.Language.Value diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 35e579d2a..e833fa7eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -111,8 +111,25 @@ namespace Barotrauma.Networking ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); + try + { + IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); + ProcessP2PData(inc); + } + catch (Exception e) + { + string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; + GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + } + private void ProcessP2PData(IReadMessage inc) + { var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); if (!packetHeader.IsServerMessage()) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 5c77d37c7..27f6e1724 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -141,15 +141,31 @@ namespace Barotrauma.Networking if (remotePeer.DisconnectTime != null) { return; } - var peerPacketHeaders = INetSerializableStruct.Read(inc); - - PacketHeader packetHeader = peerPacketHeaders.PacketHeader; + try + { + ProcessP2PData(steamId, remotePeer, inc); + } + catch (Exception e) + { + string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; + GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + } - if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) + private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc) + { + var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read(inc); + + if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep()) { remotePeer.DisconnectTime = null; - ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); + ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing"); if (initialization == ConnectionInitialization.SteamTicketAndVersion) { remotePeer.Authenticating = true; @@ -181,6 +197,7 @@ namespace Barotrauma.Networking ForwardToServerProcess(outMsg); } + } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 0c778b22c..23a9f6bef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -428,7 +428,7 @@ namespace Barotrauma.Networking if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; - ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); + ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray(); bool getBool(string key) { @@ -437,8 +437,34 @@ namespace Barotrauma.Networking } } - private static ContentPackageInfo[] ExtractContentPackageInfo(Func valueGetter) + private static ContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func valueGetter) { + //workaround to ServerRules queries truncating the values to 255 bytes + int individualPackageIndex = 0; + string? individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + if (!individualPackage.IsNullOrEmpty()) + { + List contentPackages = new List(); + do + { + string[] splitPackageInfo = individualPackage.Split(','); + if (splitPackageInfo.Length != 3) + { + DebugConsole.Log( + $"Error in a server's content package list: malformed content package info ({individualPackage})."); + return Array.Empty(); + } + string name = splitPackageInfo[0]; + string hash = splitPackageInfo[1]; + ulong.TryParse(splitPackageInfo[2], out ulong id); + contentPackages.Add(new ContentPackageInfo(name, hash, Option.Some(new SteamWorkshopId(id)))); + + individualPackageIndex++; + individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + } while (!individualPackage.IsNullOrEmpty()); + return contentPackages.ToArray(); + } + string? joinedNames = valueGetter("contentpackage"); string? joinedHashes = valueGetter("contentpackagehash"); string? joinedWorkshopIds = valueGetter("contentpackageid"); @@ -448,9 +474,11 @@ namespace Barotrauma.Networking #warning TODO: genericize ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); - if (contentPackageNames.Length != contentPackageHashes.Length - || contentPackageHashes.Length != contentPackageIds.Length) + if (contentPackageNames.Length != contentPackageHashes.Length || contentPackageHashes.Length != contentPackageIds.Length) { + DebugConsole.Log( + $"The number of names, hashes and Workshop IDs on server \"{serverName}\"" + + $" doesn't match: {contentPackageNames.Length} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Length} hashes, {contentPackageIds.Length} ids)"); return Array.Empty(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs index ff9079caf..678d89cdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -35,7 +35,7 @@ namespace Barotrauma } private static Option InfoFromListEntry(Steamworks.Data.ServerInfo entry) => - entry.Name.IsNullOrEmpty() + entry.Name.IsNullOrEmpty() || entry.Address is null ? Option.None() : Option.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs index fe80749e5..caf2e0a20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -71,10 +71,10 @@ namespace Barotrauma foreach (var lobby in lobbies) { - string lobbyOwnerStr = lobby.GetData("lobbyowner"); + string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? ""; lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); - string serverName = lobby.GetData("name"); + string serverName = lobby.GetData("name") ?? ""; if (string.IsNullOrEmpty(serverName)) { continue; } var ownerId = SteamId.Parse(lobbyOwnerStr); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 339e6051b..2039ad51d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -9,6 +9,9 @@ namespace Barotrauma.Networking { partial class ServerSettings : ISerializableEntity { + private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip"); + private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖"); + partial class NetPropertyData { public GUIComponent GUIComponent; @@ -28,7 +31,15 @@ namespace Barotrauma.Networking if (GUIComponent == null) return null; else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; else if (GUIComponent is GUITextBox textBox) return textBox.Text; - else if (GUIComponent is GUIScrollBar scrollBar) return scrollBar.BarScrollValue; + else if (GUIComponent is GUIScrollBar scrollBar) + { + if (property.PropertyType == typeof(int)) + { + return (int)MathF.Floor(scrollBar.BarScrollValue); + } + + return scrollBar.BarScrollValue; + } else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUINumberInput numInput) @@ -44,9 +55,9 @@ namespace Barotrauma.Networking else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; else if (GUIComponent is GUIScrollBar scrollBar) { - if (value.GetType() == typeof(int)) + if (value is int i) { - scrollBar.BarScrollValue = (int)value; + scrollBar.BarScrollValue = i; } else { @@ -941,11 +952,58 @@ namespace Barotrauma.Networking return true; }; + + GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false); + GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + // karma -------------------------------------------------------------------------- - var karmaBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsUseKarma")); + var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma")); GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); + var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) + { + ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") + }; + GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection); + + CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); + LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; + maxPacketSlider.Step = 0.001f; + maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); + maxPacketSlider.ToolTip = packetAmountTooltip; + maxPacketSlider.OnMoved = (scrollBar, _) => + { + GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; + int value = (int)MathF.Floor(scrollBar.BarScrollValue); + + LocalizedString valueText = value > PacketLimitMin + ? value.ToString() + : TextManager.Get("ServerSettingsNoLimit"); + + switch (value) + { + case <= PacketLimitMin: + textBlock.TextColor = GUIStyle.Green; + scrollBar.ToolTip = packetAmountTooltip; + break; + case < PacketLimitWarning: + textBlock.TextColor = GUIStyle.Red; + scrollBar.ToolTip = packetAmountTooltipWarning; + break; + default: + textBlock.TextColor = GUIStyle.TextColorNormal; + scrollBar.ToolTip = packetAmountTooltip; + break; + } + + textBlock.Text = $"{maxPacketCountLabel} {valueText}"; + return true; + }; + GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider); + maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); + karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cde031084..765d1a5d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -276,6 +276,8 @@ namespace Barotrauma.Networking if (GameMain.Client?.Character != null) { var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index d23386f33..28d0461ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -146,7 +146,7 @@ namespace Barotrauma.Networking if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak { - if (client.Character != null && !client.Character.Removed) + if (client.Character != null && !client.Character.Removed && !client.Character.IsDead) { Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f); Vector3 listenerPos = GameMain.SoundManager.ListenerPosition; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 3da38f7df..1440b125b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -169,7 +169,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index c23b2b190..f3e1cfdaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -18,7 +18,7 @@ namespace Barotrauma public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } private GUIButton nextButton; - private GUILayoutGroup characterInfoColumns; + private GUIListBox characterInfoColumns; public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) : base(newGameContainer, loadGameContainer) @@ -249,11 +249,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); - characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; + characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true); var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) @@ -306,8 +302,8 @@ namespace Barotrauma for (int i = 0; i < characterInfos.Count; i++) { - var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), - characterInfoColumns.RectTransform)); + var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f), + characterInfoColumns.Content.RectTransform)); var (characterInfo, job) = characterInfos[i]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 6cb2671ea..1cefb2a67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -202,18 +202,21 @@ namespace Barotrauma case CampaignMode.InteractionType.PurchaseSub: submarineSelection?.Update(); break; - case CampaignMode.InteractionType.Crew: CrewManagement?.Update(); break; - case CampaignMode.InteractionType.Store: Store?.Update(deltaTime); - break; - + break; case CampaignMode.InteractionType.MedicalClinic: MedicalClinic?.Update(deltaTime); break; + case CampaignMode.InteractionType.Map: + if (StartButton != null) + { + StartButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && Character.Controlled is { IsIncapacitated: false }; + } + break; } } @@ -568,7 +571,6 @@ namespace Barotrauma StartButton.Visible = false; missionList.Enabled = false; } - //locationInfoPanel?.UpdateAuto(1.0f); } public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 753b14915..24e67f496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -343,7 +343,7 @@ namespace Barotrauma editorContainer.ClearChildren(); paramsList.Content.ClearChildren(); - foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) + foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Identifier.Value) @@ -359,7 +359,7 @@ namespace Barotrauma editorContainer.ClearChildren(); caveParamsList.Content.ClearChildren(); - foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) + foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -375,7 +375,7 @@ namespace Barotrauma editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); - foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) + foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams.OrderBy(p => p.Identifier)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -391,7 +391,7 @@ namespace Barotrauma editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); - foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 244167045..74ad6abde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2244,9 +2244,9 @@ namespace Barotrauma List rankOptions = new List(); foreach (PermissionPreset rank in PermissionPreset.List) { - rankOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => + rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () => { - LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); + LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate @@ -2350,7 +2350,7 @@ namespace Barotrauma }; foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - rankDropDown.AddItem(permissionPreset.Name, permissionPreset, permissionPreset.Description); + rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description); } rankDropDown.AddItem(TextManager.Get("CustomRank"), null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 064f9819e..34461d263 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,16 +1,16 @@ using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Xml.Linq; -using Microsoft.Xna.Framework.Input; -using Barotrauma.IO; -using Barotrauma.Steam; namespace Barotrauma { @@ -3546,10 +3546,45 @@ namespace Barotrauma TextManager.Get("LoadingVanillaSubmarineHeader"), TextManager.Get("LoadingVanillaSubmarineDesc")); - public void LoadSub(SubmarineInfo info) + public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true) { Submarine.Unload(); Submarine selectedSub = null; + + if (checkIdConflicts) + { + Dictionary entities = new Dictionary(); + foreach (var subElement in info.SubmarineElement.Elements()) + { + int id = subElement.GetAttributeInt("ID", -1); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", string.Empty); + if (entities.TryGetValue(id, out Identifier duplicateEntity)) + { + var errorMsg = new GUIMessageBox( + TextManager.Get("error"), + TextManager.GetWithVariables("subeditor.duplicateiderror", + ("[entity1]", $"{duplicateEntity} ({id})"), + ("[entity2]", $"{identifier} ({id})")), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + errorMsg.Buttons[0].OnClicked = (bnt, userdata) => + { + subElement.Remove(); + LoadSub(info, checkIdConflicts: false); + errorMsg.Close(); + return true; + }; + errorMsg.Buttons[1].OnClicked = (bnt, userdata) => + { + LoadSub(info, checkIdConflicts: false); + errorMsg.Close(); + return true; + }; + return; + } + entities.Add(id, identifier); + } + } + try { selectedSub = new Submarine(info); @@ -5263,7 +5298,7 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) + if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default) { if (dummyCharacter != null) { @@ -5354,6 +5389,16 @@ namespace Barotrauma else { var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + foreach (var item in Item.ItemList) + { + //attached wires are not normally selectable (by clicking), + //but let's select them manually when selecting all + var wire = item.GetComponent(); + if (wire != null && wire.Connections.None(c => c == null) && !selectables.Contains(item)) + { + selectables.Add(item); + } + } lock (selectables) { selectables.ForEach(MapEntity.AddSelection); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 27c008170..264cf243f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Steam CanBeFocused = false }; var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform), - text: item.Title); + text: item.Title ?? ""); var itemDownloadProgress = new GUIProgressBar(new RectTransform((0.5f, 0.75f), itemFrame.RectTransform, Anchor.CenterRight), 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 1c1cda216..831abe5b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -105,7 +105,8 @@ 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.UgcId))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp + => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 26b32f26c..918437918 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -16,6 +16,9 @@ namespace Barotrauma.Steam private static readonly List initializationErrors = new List(); public static IReadOnlyList InitializationErrors => initializationErrors; + private static bool IsInitializedProjectSpecific + => Steamworks.SteamClient.IsValid && Steamworks.SteamClient.IsLoggedOn; + private static void InitializeProjectSpecific() { if (IsInitialized) { return; } @@ -23,7 +26,6 @@ namespace Barotrauma.Steam try { Steamworks.SteamClient.Init(AppID, false); - IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; if (IsInitialized) { @@ -43,13 +45,11 @@ namespace Barotrauma.Steam } catch (DllNotFoundException) { - IsInitialized = false; initializationErrors.Add("SteamDllNotFound".ToIdentifier()); } catch (Exception e) { DebugConsole.ThrowError("SteamManager initialization threw an exception", e); - IsInitialized = false; initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 124e77d1d..ed270c344 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Steam { await Task.Yield(); - string thumbnailUrl = item.PreviewImageUrl; + string? thumbnailUrl = item.PreviewImageUrl; if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } var client = new RestClient(thumbnailUrl); var request = new RestRequest(".", Method.GET); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 415bda37b..779101b67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Steam private string ExtractTitle(ItemOrPackage itemOrPackage) => itemOrPackage.TryGet(out ContentPackage package) ? package.Name - : ((Steamworks.Ugc.Item)itemOrPackage).Title; + : (((Steamworks.Ugc.Item)itemOrPackage).Title ?? ""); private void CreateWorkshopItemDetailContainer( GUIFrame parent, @@ -340,6 +340,8 @@ namespace Barotrauma.Steam subscribeButton.OnClicked = (button, o) => { + if (!SteamManager.IsInitialized) { return false; } + if (!workshopItem.IsSubscribed) { workshopItem.Subscribe(); @@ -360,6 +362,8 @@ namespace Barotrauma.Steam new RectTransform(Vector2.Zero, subscribeButton.RectTransform), onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + if (subscribeButtonSprite.Style is { Identifier: { } styleId }) { if (workshopItem.IsSubscribed && styleId != minusButton) @@ -380,6 +384,8 @@ namespace Barotrauma.Steam new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), onDraw: (spriteBatch, component) => { + if (!SteamManager.IsInitialized) { return; } + bool visible = workshopItem.IsSubscribed && (workshopItem.IsDownloading || workshopItem.IsDownloadPending @@ -407,6 +413,8 @@ namespace Barotrauma.Steam }, onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + displayedDownloadAmount = Math.Min( workshopItem.DownloadAmount, MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); @@ -450,7 +458,7 @@ namespace Barotrauma.Steam var title = new GUITextBlock( new RectTransform(Vector2.One, itemLayout.RectTransform), - workshopItem.Title, font: GUIStyle.Font) + workshopItem.Title ?? "", font: GUIStyle.Font) { CanBeFocused = false }; @@ -570,7 +578,7 @@ namespace Barotrauma.Steam var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); var selectedTitle = - new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, + new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title ?? "", font: GUIStyle.LargeFont); var author = workshopItem.Owner; @@ -682,9 +690,9 @@ namespace Barotrauma.Steam TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => { - authorButton.Text = author.Name; + authorButton.Text = author.Name ?? ""; authorButton.RectTransform.NonScaledSize = - ((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), + ((int)(authorButton.Font.MeasureString(author.Name ?? "").X + authorPadding.X + authorPadding.Z), authorButton.RectTransform.NonScaledSize.Y); }); @@ -769,7 +777,7 @@ namespace Barotrauma.Steam var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); - CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + CreateTagsList((workshopItem.Tags ?? Array.Empty()).ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs new file mode 100644 index 000000000..5fec7cc05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal class LeakyBucket + { + private readonly Queue queue; + private readonly int capacity; + private readonly float cooldownInSeconds; + private float timer; + + public LeakyBucket(float cooldownInSeconds, int capacity) + { + this.cooldownInSeconds = cooldownInSeconds; + this.capacity = capacity; + queue = new Queue(capacity); + } + + public void Update(float deltaTime) + { + if (timer > 0f) + { + timer -= deltaTime; + return; + } + + if (queue.Count is 0) { return; } + + TryDequeue(); + } + + private void TryDequeue() + { + timer = cooldownInSeconds; + if (queue.TryDequeue(out var action)) + { + action.Invoke(); + } + } + + public bool TryEnqueue(Action item) + { + if (queue.Count >= capacity) { return false; } + queue.Enqueue(item); + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 272f9e4c9..c5342a37c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -142,11 +142,17 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); - Submarine.Draw(spriteBatch); - Submarine.DrawFront(spriteBatch); - Submarine.DrawDamageable(spriteBatch, null); - spriteBatch.End(); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f))); + DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true)); + DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true)); + + void DrawBatch(Action drawAction) + { + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); + drawAction.Invoke(); + spriteBatch.End(); + } GameMain.Instance.GraphicsDevice.SetRenderTarget(null); GameMain.Instance.GraphicsDevice.Viewport = prevViewport; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 26fdc0e9b..89803f6bf 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 1421aa367..bd8000122 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 3db6a3776..036739a1a 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -14,7 +14,7 @@ Debug;Release;Unstable true app.manifest - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index b2c29b9fd..9c21d07a3 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index c8e74a308..2bc4f2cf0 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 25c94259e..0411715e8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -13,6 +13,15 @@ namespace Barotrauma { static partial class DebugConsole { + private static readonly RateLimiter rateLimiter = new( + maxRequests: 50, + expiryInSeconds: 5, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + public partial class Command { /// @@ -608,12 +617,12 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => { - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { ThrowError("Rank \"" + rank + "\" not found."); @@ -622,7 +631,7 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - NewMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + NewMessage("Assigned the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); }, args, 1); }); @@ -1992,6 +2001,7 @@ namespace Barotrauma "freecam", (Client client, Vector2 cursorWorldPos, string[] args) => { + client.UsingFreeCam = true; GameMain.Server.SetClientCharacter(client, null); client.SpectateOnly = true; } @@ -2105,7 +2115,7 @@ namespace Barotrauma } string rank = string.Join("", args.Skip(1)); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); @@ -2114,8 +2124,8 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); - NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient); + NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); } ); @@ -2509,26 +2519,37 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { item.TryCreateServerEventSpam(); - item.CreateStatusEvent(); + item.CreateStatusEvent(loadingRound: false); } foreach (Structure wall in Structure.WallList) { GameMain.Server.CreateEntityEvent(wall); } })); - commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that stalls each file transfer packet by the specified duration.", (string[] args) => + commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that makes all file transfers take at least the specified duration.", (string[] args) => { float seconds = 0.0f; if (args.Length > 0) { float.TryParse(args[0], out seconds); } - GameMain.Server.FileSender.StallPacketsTime = seconds; + GameMain.Server.FileSender.ForceMinimumFileTransferDuration = seconds; NewMessage("Set file transfer stall time to " + seconds); })); #endif } + public static void ServerRead(IReadMessage inc, Client sender) + { + string consoleCommand = inc.ReadString(); + float cursorX = inc.ReadSingle(); + float cursorY = inc.ReadSingle(); + + if (rateLimiter.IsLimitReached(sender)) { return; } + + ExecuteClientCommand(sender, new Vector2(cursorX, cursorY), consoleCommand); + } + public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command) { if (GameMain.Server == null) return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index d6695caf7..255635fb7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; using Barotrauma.Extensions; using Barotrauma.Networking; @@ -11,25 +12,16 @@ namespace Barotrauma { internal partial class MedicalClinic { - private enum RateLimitResult - { - OK, - LimitReached - } - - private struct RateLimitInfo - { - public int Requests; - public const int MaxRequests = 10; - public DateTimeOffset Expiry; - } + // allow 20 requests per 5 seconds, announce to chat if the limit is reached + private readonly RateLimiter rateLimiter = new( + maxRequests: RateLimitMaxRequests, + expiryInSeconds: RateLimitExpiry, + punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); private readonly List afflictionSubscribers = new(); - private readonly Dictionary rateLimits = new(); - public void ServerRead(IReadMessage inc, Client sender) { NetworkHeader header = (NetworkHeader)inc.ReadByte(); @@ -65,7 +57,7 @@ namespace Barotrauma private void ProcessNewAddition(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); @@ -74,7 +66,7 @@ namespace Barotrauma private void ProcessAddEverything(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } AddEverythingToPending(); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } @@ -92,7 +84,7 @@ namespace Barotrauma private void ProcessNewRemoval(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetRemovedAffliction removed = INetSerializableStruct.Read(inc); RemovePendingAffliction(removed.CrewMember, removed.Affliction); @@ -101,14 +93,14 @@ namespace Barotrauma private void ProcessRequestedPending(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); @@ -116,7 +108,7 @@ namespace Barotrauma private void ProcessClearing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } if (!PendingHeals.Any()) { return; } @@ -126,7 +118,7 @@ namespace Barotrauma private void ProcessRequestedAfflictions(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember crewMember = INetSerializableStruct.Read(inc); @@ -135,6 +127,17 @@ namespace Barotrauma ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; + if (foundInfo is null) + { + StringBuilder sb = new(); + foreach (CharacterInfo character in GetCrewCharacters()) + { + sb.AppendLine($" - {character.DisplayName} ({character.ID})"); + } + + DebugConsole.ThrowError($"Could not find the requested crew member with ID {crewMember.CharacterInfoID}.\n{sb}"); + } + if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); @@ -158,32 +161,6 @@ namespace Barotrauma ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } - private RateLimitResult CheckRateLimit(Client client) - { - if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo)) - { - if (rateLimitInfo.Expiry < DateTimeOffset.Now) - { - rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5); - rateLimitInfo.Requests = 1; - } - else - { - if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; } - - rateLimitInfo.Requests++; - } - - rateLimits[client] = rateLimitInfo; - } - else - { - rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) }); - } - - return RateLimitResult.OK; - } - private IWriteMessage StartSending() { IWriteMessage msg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 65b176cff..84c67ceae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -135,6 +136,8 @@ namespace Barotrauma } } + EnsureItemsInBothHands(c.Character); + CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { @@ -174,6 +177,33 @@ namespace Barotrauma } } + private void EnsureItemsInBothHands(Character character) + { + if (this is not CharacterInventory charInv) { return; } + + int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), + rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); + + if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; } + + TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); + TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); + + void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot) + { + const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand; + + foreach (Item it in slots[originalSlot].Items) + { + if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; } + + TryPutItem(it, otherHandSlot, true, true, character, false); + } + } + + bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length; + } + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 40c1ee0a2..a48f13895 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -65,7 +65,8 @@ namespace Barotrauma msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case ItemStatusEventData _: + case ItemStatusEventData statusEvent: + msg.WriteBoolean(statusEvent.LoadingRound); msg.WriteSingle(condition); break; case AssignCampaignInteractionEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index c285765e3..e9216ab7b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -102,9 +102,9 @@ namespace Barotrauma.Networking similarity *= 0.25f; } - bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection; + bool isSpamExempt = RateLimiter.IsExempt(c); - if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner) + if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) { GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); @@ -125,7 +125,7 @@ namespace Barotrauma.Networking c.ChatSpamSpeed += similarity + 0.5f; - if (c.ChatSpamTimer > 0.0f && !isOwner) + if (c.ChatSpamTimer > 0.0f && !isSpamExempt) { ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index eae96b706..9b8ab528d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -108,9 +108,7 @@ namespace Barotrauma.Networking const int MaxTransferCount = 16; const int MaxTransferCountPerRecipient = 5; - - public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0); - + public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver); public FileTransferDelegate OnStarted; public FileTransferDelegate OnEnded; @@ -121,8 +119,9 @@ namespace Barotrauma.Networking private readonly ServerPeer peer; + public static DateTime StartTime; #if DEBUG - public float StallPacketsTime { get; set; } + public float ForceMinimumFileTransferDuration { get; set; } #endif public IReadOnlyList ActiveTransfers => activeTransfers; @@ -172,6 +171,8 @@ namespace Barotrauma.Networking return null; } + StartTime = DateTime.Now; + OnStarted(transfer); GameMain.Server.LastClientListUpdateID++; @@ -259,7 +260,18 @@ namespace Barotrauma.Networking for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) { long remaining = transfer.Data.Length - transfer.SentOffset; - int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); +#if DEBUG + bool stalling = false; + float elapsedTime = (float)(DateTime.Now - StartTime).TotalSeconds; + if (elapsedTime < ForceMinimumFileTransferDuration) + { + int remainingChunks = (int)Math.Max(remaining / chunkLen, 1); + transfer.WaitTimer = + Math.Max(transfer.WaitTimer, (ForceMinimumFileTransferDuration - elapsedTime) / remainingChunks); + if (remainingChunks <= 1) { break; } + } +#endif + int sendByteCount = remaining > chunkLen ? chunkLen : (int)remaining; message = new WriteOnlyMessage(); message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); @@ -293,11 +305,10 @@ namespace Barotrauma.Networking //this gets reset when packet loss or disorder sets in transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, transfer.PacketsPerUpdate + 0.05f); - } - #if DEBUG - transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); + if (stalling) { break; } #endif + } } catch (Exception e) @@ -330,7 +341,7 @@ namespace Barotrauma.Networking { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); - if (matchingTransfer != null) CancelTransfer(matchingTransfer); + if (matchingTransfer != null) { CancelTransfer(matchingTransfer); } return; } else if (messageType == FileTransferMessageType.Data) @@ -359,6 +370,7 @@ namespace Barotrauma.Networking if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) { matchingTransfer.Status = FileTransferStatus.Finished; + DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}"); } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index a4ffcda9c..3cf292b13 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Networking } else { - var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); + var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None"); if (defaultPerms != null) { newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); @@ -332,9 +332,8 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { -#if CLIENT - if (ShowNetStats) { netStats.Update(deltaTime); } -#endif + dosProtection.Update(deltaTime); + if (!started) { return; } if (ChildServerRelay.HasShutDown) @@ -387,10 +386,10 @@ namespace Barotrauma.Networking Voting.Update(deltaTime); bool isCrewDead = - connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); + connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated)); bool subAtLevelEnd = false; - if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) + if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode) { if (Level.Loaded?.EndOutpost != null) { @@ -692,10 +691,14 @@ namespace Barotrauma.Networking } } + private readonly DoSProtection dosProtection = new(); + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); + using var _ = dosProtection.Start(connectedClient); + ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { @@ -772,9 +775,12 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { - ServerSettings.CampaignSettings = settings; - ServerSettings.SaveSettings(); - MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); + using (dosProtection.Pause(connectedClient)) + { + ServerSettings.CampaignSettings = settings; + ServerSettings.SaveSettings(); + MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); + } } } } @@ -784,11 +790,14 @@ namespace Barotrauma.Networking if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); - return; + break; } if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) - { - MultiPlayerCampaign.LoadCampaign(saveName); + { + using (dosProtection.Pause(connectedClient)) + { + MultiPlayerCampaign.LoadCampaign(saveName); + } } } break; @@ -1085,7 +1094,7 @@ namespace Barotrauma.Networking ChatMessage.ServerRead(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; default: return SegmentTableReader.BreakSegmentReading.Yes; @@ -1246,7 +1255,7 @@ namespace Barotrauma.Networking entityEventManager.Read(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; case ClientNetSegment.SpectatingPos: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); @@ -1406,19 +1415,23 @@ namespace Barotrauma.Networking bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + using (dosProtection.Pause(sender)) { - mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - mpCampaign.UpdateStoreStock(); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + { + mpCampaign.SavePlayers(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + mpCampaign.UpdateStoreStock(); + GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + save = false; + } + EndGame(wasSaved: save); } - else - { - save = false; - } - EndGame(wasSaved: save); } else if (mpCampaign != null) { @@ -1442,45 +1455,54 @@ namespace Barotrauma.Networking } else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + using (dosProtection.Pause(sender)) + { + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + } } } else if (!GameStarted && !initiatedStartGame) { - Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - TryStartGame(); + using (dosProtection.Pause(sender)) + { + Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); + TryStartGame(); + } } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { - var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); - //don't force location if we've teleported - bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; - switch (availableTransition) + using (dosProtection.Pause(sender)) { - case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SelectLocation( - mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.None: -#if DEBUG || UNSTABLE - DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); -#endif - return; - default: - Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - mpCampaign.LoadNewLevel(); - break; + var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); + //don't force location if we've teleported + bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; + switch (availableTransition) + { + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SelectLocation( + mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.None: + #if DEBUG || UNSTABLE + DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); + #endif + break; + default: + Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + mpCampaign.LoadNewLevel(); + break; + } } } } @@ -1520,11 +1542,7 @@ namespace Barotrauma.Networking mpCampaign?.ServerRead(inc, sender); break; case ClientPermissions.ConsoleCommands: - { - string consoleCommand = inc.ReadString(); - Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); - DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand); - } + DebugConsole.ServerRead(inc, sender); break; case ClientPermissions.ManagePermissions: byte targetClientID = inc.ReadByte(); @@ -2582,16 +2600,20 @@ namespace Barotrauma.Networking { if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } - const int MaxSaves = 255; - var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); - for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + using (dosProtection.Pause(client)) { - msg.WriteNetSerializableStruct(saveInfos[i]); + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + { + msg.WriteNetSerializableStruct(saveInfos[i]); + } + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + return true; } @@ -3588,15 +3610,28 @@ namespace Barotrauma.Networking } } + private readonly RateLimiter charInfoRateLimiter = new( + maxRequests: 5, + expiryInSeconds: 10, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + private void UpdateCharacterInfo(IReadMessage message, Client sender) { - sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); - if (sender.SpectateOnly) - { - return; - } + bool spectateOnly = message.ReadBoolean(); + message.ReadPadBits(); - string newName = message.ReadString(); + sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); + if (sender.SpectateOnly) { return; } + + var netInfo = INetSerializableStruct.Read(message); + + if (charInfoRateLimiter.IsLimitReached(sender)) { return; } + + string newName = netInfo.NewName; if (string.IsNullOrEmpty(newName)) { newName = sender.Name; @@ -3614,42 +3649,31 @@ namespace Barotrauma.Networking } } - int tagCount = message.ReadByte(); - HashSet tagSet = new HashSet(); - for (int i = 0; i < tagCount; i++) - { - tagSet.Add(message.ReadIdentifier()); - } - int hairIndex = message.ReadByte(); - int beardIndex = message.ReadByte(); - int moustacheIndex = message.ReadByte(); - int faceAttachmentIndex = message.ReadByte(); - Color skinColor = message.ReadColorR8G8B8(); - Color hairColor = message.ReadColorR8G8B8(); - Color facialHairColor = message.ReadColorR8G8B8(); - - List jobPreferences = new List(); - int count = message.ReadByte(); - for (int i = 0; i < Math.Min(count, 3); i++) - { - string jobIdentifier = message.ReadString(); - int variant = message.ReadByte(); - if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab)) - { - if (jobPrefab.HiddenJob) { continue; } - jobPreferences.Add(new JobVariant(jobPrefab, variant)); - } - } - sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); - sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - sender.CharacterInfo.Head.SkinColor = skinColor; - sender.CharacterInfo.Head.HairColor = hairColor; - sender.CharacterInfo.Head.FacialHairColor = facialHairColor; - if (jobPreferences.Count > 0) + sender.CharacterInfo.RecreateHead( + tags: netInfo.Tags.ToImmutableHashSet(), + hairIndex: netInfo.HairIndex, + beardIndex: netInfo.BeardIndex, + moustacheIndex: netInfo.MoustacheIndex, + faceAttachmentIndex: netInfo.FaceAttachmentIndex); + + sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor; + sender.CharacterInfo.Head.HairColor = netInfo.HairColor; + sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor; + + if (netInfo.JobVariants.Length > 0) { - sender.JobPreferences = jobPreferences; + List variants = new List(); + foreach (NetJobVariant jv in netInfo.JobVariants) + { + if (jv.ToJobVariant() is { } variant) + { + variants.Add(variant); + } + } + + sender.JobPreferences = variants; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index b8e4393bf..143ac4dae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -24,11 +24,28 @@ namespace Barotrauma.Networking } protected readonly Callbacks callbacks; + private readonly ImmutableArray contentPackages; protected ServerPeer(Callbacks callbacks) { this.callbacks = callbacks; - } + + List contentPackageList = new List(); + foreach (var cp in ContentPackageManager.EnabledPackages.All) + { + if (!cp.Files.Any()) { continue; } + if (!cp.HasMultiplayerSyncedContent && !cp.Files.All(f => f is SubmarineFile)) { continue; } + if (cp.UgcId.TryUnwrap(out var id1) && + contentPackageList.FirstOrDefault(cp => cp.UgcId.TryUnwrap(out var id2) && id1.Equals(id2)) is ContentPackage existingPackage) + { + //there can be multiple enabled mods with the same UgcId if the player has e.g. created a local copy of a workshop mod + DebugConsole.AddWarning($"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package."); + continue; + } + contentPackageList.Add(cp); + } + contentPackages = contentPackageList.ToImmutableArray(); + } public abstract void InitializeSteamServerCallbacks(); @@ -250,9 +267,7 @@ namespace Barotrauma.Networking structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, - ContentPackages = ContentPackageManager.EnabledPackages.All - .Where(cp => cp.Files.Any()) - .Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) + ContentPackages = contentPackages .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .ToImmutableArray() }; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index d3ef072bb..55a116898 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -520,7 +520,7 @@ namespace Barotrauma.Networking else { string presetName = clientElement.GetAttributeString("preset", ""); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == presetName); if (preset == null) { DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found."); @@ -585,8 +585,7 @@ namespace Barotrauma.Networking foreach (SavedClientPermission clientPermission in ClientPermissions) { var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); - #warning TODO: this is broken because of localization - if (matchingPreset != null && matchingPreset.Name == "None") + if (matchingPreset != null && matchingPreset.Identifier == "None") { continue; } @@ -600,7 +599,7 @@ namespace Barotrauma.Networking clientElement.Add(matchingPreset == null ? new XAttribute("permissions", clientPermission.Permissions.ToString()) - : new XAttribute("preset", matchingPreset.Name)); + : new XAttribute("preset", matchingPreset.DisplayName)); if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 787c786d6..4af593e86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -225,7 +225,7 @@ namespace Barotrauma } } - public void ServerRead(IReadMessage inc, Client sender) + public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection) { if (GameMain.Server == null || sender == null) { return; } @@ -337,7 +337,10 @@ namespace Barotrauma inc.ReadPadBits(); - GameMain.Server.UpdateVoteStatus(); + using (dosProtection.Pause(sender)) + { + GameMain.Server.UpdateVoteStatus(); + } } public void ServerWrite(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 993323acb..2a127c1ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -158,7 +158,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index eaa70b999..56b57530c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -5,12 +5,13 @@ namespace Barotrauma.Steam { partial class SteamManager { - private static void InitializeProjectSpecific() { IsInitialized = true; } + private static void InitializeProjectSpecific() { } + + private static bool IsInitializedProjectSpecific + => Steamworks.SteamServer.IsValid; public static bool CreateServer(Networking.GameServer server, bool isPublic) { - IsInitialized = true; - Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { GamePort = (ushort)server.Port, @@ -56,10 +57,15 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.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.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); + int index = 0; + foreach (var contentPackage in contentPackages) + { + string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; + Steamworks.SteamServer.SetKey( + $"contentpackage{index}", + contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr); + index++; + } 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/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index ba77838e6..f8ce8c1d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -20,19 +20,6 @@ namespace Barotrauma } public delegate void MessageSender(string message); - public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender) - { - string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, - ("[codewords]", codeWords), - ("[coderesponse]", codeResponse)); - messageSender(greetingMessage); - Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character); - Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection); - if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null) - { - GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox); - } - } public void SendChatMessage(string serverText, Identifier iconIdentifier) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index d2397ec46..cbaeabea5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -224,10 +224,6 @@ namespace Barotrauma { pendingMessages.Add(traitor, new List()); } - foreach (var traitor in Traitors.Values) - { - traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message)); - } pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs new file mode 100644 index 000000000..faf50c98a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs @@ -0,0 +1,232 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal sealed class DoSProtection + { + /// + /// A struct that executes an action when it's created and another one when it's disposed. + /// + public readonly ref struct DoSAction + { + private readonly Client sender; + private readonly Action end; + + public DoSAction(Client sender, Action start, Action end) + { + this.sender = sender; + this.end = end; + start(sender); + } + + public void Dispose() + { + end(sender); + } + } + + private sealed class OffenseData + { + /// + /// Timer that keeps track of how long it takes to process a packet. + /// + public readonly Stopwatch Stopwatch = new(); + + /// + /// Amount of strikes the client has received for causing the server to slow down. + /// + public int Strikes; + + /// + /// How many packets have been sent in the last minute. + /// + public int PacketCount; + + /// + /// Resets the strikes and packet count. + /// + public void ResetStrikes() + { + Strikes = 0; + PacketCount = 0; + } + + /// + /// Resets the timer. + /// + public void ResetTimer() => Stopwatch.Reset(); + } + + private readonly Dictionary clients = new(); + + private float stopwatchResetTimer, + strikesResetTimer; + + private const int StopwatchResetInterval = 1, + StrikesResetInterval = 60, + StrikeThreshold = 6; + + /// + /// Called when the server receives a packet to start logging how much time it takes to process. + /// + /// The client to start a timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns. + /// + /// + /// + /// public void ServerRead(IReadMessage msg, Client c) + /// { + /// // start the timer + /// using var _ = dosProtection.Start(connectedClient); + /// + /// if (condition) + /// { + /// // the timer will be stopped here. + /// return; + /// } + /// + /// ProcessMessage(msg); + /// // the timer will be stopped here. + /// } + /// + /// + public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor); + + /// + /// Temporary pauses the timer for the client. + /// Used when we know a packet is going to slow down the server but we don't want to count it as a strike. + /// For example when a client is starting a round. + /// + /// The client to pause the timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling resume is not required, the timer will be resumed automatically when the using block ends. + /// + /// + /// + /// using (dos.Pause(client)) + /// { + /// // do something that will slow down the server + /// } + /// // the timer will be resumed here + /// + /// + public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor); + + private void StartFor(Client client) + { + if (!clients.ContainsKey(client)) + { + clients.Add(client, new OffenseData()); + } + + clients[client].Stopwatch.Start(); + } + + private void EndFor(Client client) + { + if (GetData(client) is not { } data) { return; } + + data.PacketCount++; + data.Stopwatch.Stop(); + UpdateOffense(client, data); + } + + // stops the clock but doesn't update offenses + private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop(); + + private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start(); + + private void UpdateOffense(Client client, OffenseData data) + { + if (GameMain.Server?.ServerSettings is not { } settings) { return; } + + // client is sending too many packets, kick them + if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin) + { + AttemptKickClient(client, TextManager.Get("PacketLimitKicked")); + clients.Remove(client); + return; + } + + // if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong + if (data.Stopwatch.ElapsedMilliseconds < 100) { return; } + + data.Strikes++; + data.ResetTimer(); + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection); + + // too many strikes, get them out of here + if (data.Strikes < StrikeThreshold) { return; } + + if (settings.EnableDoSProtection) + { + AttemptKickClient(client, TextManager.Get("DoSProtectionKicked")); + } + + clients.Remove(client); + + static void AttemptKickClient(Client client, LocalizedString reason) + { + // ReSharper disable once ConvertToConstant.Local + bool doesRateLimitAffectClient = +#if DEBUG + true; // for testing +#else + !RateLimiter.IsExempt(client); +#endif + + if (!doesRateLimitAffectClient) + { + return; + } + + GameMain.Server?.KickClient(client, reason.Value); + } + } + + public void Update(float deltaTime) + { + stopwatchResetTimer += deltaTime; + strikesResetTimer += deltaTime; + + // reset the stopwatch every second + if (stopwatchResetTimer > StopwatchResetInterval) + { + stopwatchResetTimer = 0; + foreach (OffenseData data in clients.Values) + { + data.ResetTimer(); + } + } + + // reset the strikes every minute + if (strikesResetTimer > StrikesResetInterval) + { + strikesResetTimer = 0; + foreach (var (client, data) in clients) + { + if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings) + { + if (data.PacketCount > settings.MaxPacketAmount * 0.9f) + { + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection); + } + } + + data.ResetStrikes(); + } + } + } + + private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs new file mode 100644 index 000000000..4c6f141c9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Networking; + +namespace Barotrauma +{ + public enum RateLimitAction + { + Invalid, + OnLimitReached, + OnLimitDoubled, + } + + public enum RateLimitPunishment + { + None, // just ignore + Announce, // announce to the server + Kick, // kick the player + Ban // ban the player + } + + internal sealed class RateLimiter + { + private sealed record RateLimit(DateTimeOffset Expiry) + { + public int RequestAmount; + } + + private readonly Dictionary rateLimits = new(); + private readonly HashSet expiredRateLimits = new(); + private readonly Dictionary recentlyAnnouncedOffenders = new(); + + private readonly int maxRequests, expiryInSeconds; + + private readonly ImmutableDictionary punishments; + + public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules) + { + this.maxRequests = maxRequests; + this.expiryInSeconds = expiryInSeconds; + + punishments = punishmentRules.ToImmutableDictionary( + static pair => pair.Action, + static pair => pair.Punishment); + } + + public bool IsLimitReached(Client client) + { +#if !DEBUG + if (IsExempt(client)) { return false; } +#endif + expiredRateLimits.Clear(); + + foreach (var (c, limit) in rateLimits) + { + if (limit.Expiry < DateTimeOffset.Now) + { + expiredRateLimits.Add(c); + } + } + + foreach (Client c in expiredRateLimits) + { + rateLimits.Remove(c); + } + + if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit)) + { + rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + rateLimits.Add(client, rateLimit); + } + + rateLimit.RequestAmount++; + + if (rateLimit.RequestAmount > maxRequests) + { + ProcessPunishment(client, rateLimit.RequestAmount); + return true; + } + + return false; + } + + private void ProcessPunishment(Client client, int requests) + { + bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true }; + + foreach (var (action, punishment) in punishments) + { + switch (action) + { + case RateLimitAction.Invalid: + continue; + case RateLimitAction.OnLimitReached when requests >= maxRequests: + case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2: + switch (punishment) + { + case RateLimitPunishment.None: + continue; + case RateLimitPunishment.Announce: + AnnounceOffender(client); + break; + case RateLimitPunishment.Ban when isDosProtectionEnabled: + GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + case RateLimitPunishment.Kick when isDosProtectionEnabled: + GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + } + break; + } + } + } + + private void AnnounceOffender(Client client) + { + if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry)) + { + if (expiry > DateTimeOffset.Now) { return; } + + recentlyAnnouncedOffenders.Remove(client); + } + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection); + recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + } + + public static bool IsExempt(Client client) => + (GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection) + || client.HasPermission(ClientPermissions.SpamImmunity); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 7a6026a51..b4a784950 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.9.0 + 1.0.13.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 961def545..e42ed985a 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -8,7 +8,7 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents,SpamImmunity"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 131be5ec0..e125d449d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -107,12 +107,26 @@ namespace Barotrauma } } - public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) => - steeringManager is IndoorsSteeringManager pathSteering && - pathSteering.CurrentPath != null && - (!requireUnfinished || !pathSteering.CurrentPath.Finished) && - !pathSteering.CurrentPath.Unreachable && - (!requireNonDirty || !pathSteering.IsPathDirty); + /// + /// Is the current path valid, using the provided parameters. + /// + /// + /// + /// + /// When is defined, returns false if any of the nodes fails to match the predicate. + public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func nodePredicate = null) + { + if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; } + if (pathSteering.CurrentPath == null) { return false; } + if (pathSteering.CurrentPath.Unreachable) { return false; } + if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; } + if (requireNonDirty && pathSteering.IsPathDirty) { return false; } + if (nodePredicate != null) + { + return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n)); + } + return true; + } public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null; public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable; @@ -251,7 +265,7 @@ namespace Barotrauma } private readonly HashSet unequippedItems = new HashSet(); - public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) + public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable targetTags = null) { var pickable = item.GetComponent(); if (pickable == null) { return false; } @@ -265,23 +279,28 @@ namespace Barotrauma } else { - var holdable = item.GetComponent(); - if (holdable != null) - { - pickable = holdable; - } + // Not allowed to wear -> don't use the Wearable component even when it's found. + pickable = item.GetComponent(); } if (item.ParentInventory is ItemInventory itemInventory) { if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } } - if (equip) + if (equip && pickable != null) { int targetSlot = -1; //check if all the slots required by the item are free foreach (InvSlotType slots in pickable.AllowedSlots) { if (slots.HasFlag(InvSlotType.Any)) { continue; } + if (!wear) + { + if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand)) + { + // Don't allow other than hand slots if not allowed to wear. + continue; + } + } for (int i = 0; i < targetInventory.Capacity; i++) { if (targetInventory is CharacterInventory characterInventory) @@ -294,7 +313,7 @@ namespace Barotrauma var otherItem = targetInventory.GetItemAt(i); if (otherItem == null) { continue; } //try to move the existing item to LimbSlot.Any and continue if successful - if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) + if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot)) { if (storeUnequipped && targetInventory.Owner == Character) { @@ -304,6 +323,11 @@ namespace Barotrauma } if (dropOtherIfCannotMove) { + if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags)) + { + // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory. + return false; + } //if everything else fails, simply drop the existing item otherItem.Drop(Character); } @@ -314,7 +338,7 @@ namespace Barotrauma } else { - return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); + return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot); } } @@ -339,7 +363,7 @@ namespace Barotrauma if (avoidDroppingInSea && !character.IsInFriendlySub) { // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it. - if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot)) { if (unequipMax.HasValue && ++removed >= unequipMax) { return; } continue; @@ -449,9 +473,10 @@ namespace Barotrauma Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); - if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished) + if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished) { // Very close to the target, outside, or at the end of the path -> try to steer through the gap + Character.ReleaseSecondaryItem(); SteeringManager.Reset(); pathSteering?.ResetPath(); Vector2 dir = Vector2.Normalize(diff); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 156545868..7ce6c0f4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -480,7 +480,7 @@ namespace Barotrauma if (SelectedAiTarget?.Entity != null || EscapeTarget != null) { Entity t = SelectedAiTarget?.Entity ?? EscapeTarget; - float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; + float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; } else @@ -3934,7 +3934,7 @@ namespace Barotrauma { SteerAwayFromTheEnemy(); } - else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true)) + else if (canAttackDoors && HasValidPath()) { var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 2785720c7..320cac793 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1,16 +1,16 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma { partial class HumanAIController : AIController { - public static bool debugai; + public static bool DebugAI; public static bool DisableCrewAI; private readonly AIObjectiveManager objectiveManager; @@ -55,6 +55,7 @@ namespace Barotrauma private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5; private float obstacleRaycastTimer; + private bool isBlocked; private readonly float enemyCheckInterval = 0.2f; private readonly float enemySpotDistanceOutside = 800; @@ -92,7 +93,10 @@ namespace Barotrauma private readonly SteeringManager outsideSteering, insideSteering; - public bool UseIndoorSteeringOutside { get; set; } = false; + /// + /// Waypoints that are not linked to a sub (e.g. main path). + /// + public bool UseOutsideWaypoints { get; private set; } public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; @@ -225,14 +229,15 @@ namespace Barotrauma IgnoredItems.Clear(); } - bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true) + // Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null') + bool IsCloseEnoughToTarget(float threshold, bool targetSub = true) { Entity target = SelectedAiTarget?.Entity; if (target == null) { return false; } - if (useTargetSub) + if (targetSub) { if (target.Submarine is Submarine sub) { @@ -244,62 +249,71 @@ namespace Barotrauma return false; } } - return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2); + return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold); } - bool hasValidPath = HasValidPath(); - - if (Character.Submarine == null) + bool isOutside = Character.Submarine == null; + if (isOutside) { obstacleRaycastTimer -= deltaTime; if (obstacleRaycastTimer <= 0) { + bool hasValidPath = HasValidPath(); + isBlocked = false; + UseOutsideWaypoints = false; obstacleRaycastTimer = obstacleRaycastIntervalLong; - if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false)) + ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective()?.Target; + if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false))) { // If the target is behind a level wall, switch to the pathing to get around the obstacles. - ISpatialEntity spatialTarget = SelectedAiTarget?.Entity; - if (spatialTarget == null) + IEnumerable ignoredBodies = null; + Vector2 rayEnd = spatialTarget.SimPosition; + Submarine targetSub = spatialTarget.Submarine; + if (targetSub != null) { - var gotoObjective = ObjectiveManager.GetActiveObjective(); - spatialTarget = gotoObjective?.Target; + rayEnd += targetSub.SimPosition; + ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable(); } - if (spatialTarget == null) + var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + isBlocked = obstacle != null; + // Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead. + UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin); + bool resetPath = false; + if (UseOutsideWaypoints) { - UseIndoorSteeringOutside = false; + bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null); + if (isUsingInsideWaypoints) + { + resetPath = true; + } } else { - IEnumerable ignoredBodies = null; - Vector2 rayEnd = spatialTarget.SimPosition; - Submarine targetSub = spatialTarget.Submarine; - if (targetSub != null) + bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null); + if (isUsingOutsideWaypoints) { - rayEnd += targetSub.SimPosition; - ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable(); + resetPath = true; } - var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); - UseIndoorSteeringOutside = obstacle != null; + } + if (resetPath) + { + PathSteering.ResetPath(); } } - else + else if (hasValidPath) { - UseIndoorSteeringOutside = false; - if (hasValidPath) + obstacleRaycastTimer = obstacleRaycastIntervalShort; + // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - obstacleRaycastTimer = obstacleRaycastIntervalShort; - // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) - { - PathSteering.CurrentPath.Unreachable = true; - break; - } + PathSteering.CurrentPath.Unreachable = true; + break; } } } @@ -307,10 +321,11 @@ namespace Barotrauma } else { - UseIndoorSteeringOutside = false; + UseOutsideWaypoints = false; + isBlocked = false; } - if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) + if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior @@ -352,12 +367,13 @@ namespace Barotrauma } } } - - if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) + bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer); + if (useInsideSteering) { if (steeringManager != insideSteering) { insideSteering.Reset(); + PathSteering.ResetPath(); steeringManager = insideSteering; } if (IsCloseEnoughToTarget(maxSteeringBuffer)) @@ -395,6 +411,8 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + UpdateDragged(deltaTime); + if (reportProblemsTimer > 0) { reportProblemsTimer -= deltaTime; @@ -433,10 +451,19 @@ namespace Barotrauma if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { ReportProblems(); + + } + else + { + // Allows bots to heal targets autonomously while swimming outside of the sub. + if (AIObjectiveRescueAll.IsValidTarget(Character, Character)) + { + AddTargets(Character, Character); + } } reportProblemsTimer = reportProblemsInterval; } - UpdateSpeaking(); + SpeakAboutIssues(); UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } @@ -904,17 +931,67 @@ namespace Barotrauma } })) { - suitableContainer = targetContainer; - return true; + if (targetContainer != null && + character.AIController is HumanAIController humanAI && + humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable) + { + ignoredItems.Add(targetContainer); + itemIndex = 0; + return false; + } + else + { + suitableContainer = targetContainer; + return true; + } } return false; } + private float draggedTimer; + private float refuseDraggingTimer; + /// + /// The bot breaks free if being dragged by a human player from another team for longer than this + /// + private const float RefuseDraggingThresholdHigh = 10.0f; + /// + /// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster + /// + private const float RefuseDraggingThresholdLow = 0.5f; + private const float RefuseDraggingDuration = 30.0f; + + private void UpdateDragged(float deltaTime) + { + if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; } + if (Character.IsEscorted) { return; } + if (Character.LockHands) { return; } + + //don't allow player characters who aren't in the same team to drag us for more than x seconds + if (Character.SelectedBy == null || + !Character.SelectedBy.IsPlayer || + Character.SelectedBy.TeamID == Character.TeamID) + { + refuseDraggingTimer -= deltaTime; + return; + } + + draggedTimer += deltaTime; + if (draggedTimer > RefuseDraggingThresholdHigh || + (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow)) + { + draggedTimer = 0.0f; + refuseDraggingTimer = RefuseDraggingDuration; + Character.SelectedBy.DeselectCharacter(); + Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f); + } + } + protected void ReportProblems() { Order newOrder = null; Hull targetHull = null; - bool speak = Character.SpeechImpediment < 100; + // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented + bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -1013,25 +1090,21 @@ namespace Barotrauma } if (newOrder != null && speak) { - // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented - if (!Character.IsEscorted) + if (Character.TeamID == CharacterTeamType.FriendlyNPC) { - if (Character.TeamID == CharacterTeamType.FriendlyNPC) - { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, - identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), - minDurationBetweenSimilar: 60.0f); - } - else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) - { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, + identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), + minDurationBetweenSimilar: 60.0f); + } + else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) + { + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder - .WithManualPriority(CharacterInfo.HighestManualOrderPriority) - .WithTargetEntity(targetHull) - .WithOrderGiver(Character), "", null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder + .WithManualPriority(CharacterInfo.HighestManualOrderPriority) + .WithTargetEntity(targetHull) + .WithOrderGiver(Character), "", null, Character)); #endif - } } } } @@ -1062,21 +1135,33 @@ namespace Barotrauma } } - private void UpdateSpeaking() + private void SpeakAboutIssues() { if (!Character.IsOnPlayerTeam) { return; } if (Character.SpeechImpediment >= 100) { return; } - if (Character.Oxygen < 20.0f) + float minDelay = 0.5f, maxDelay = 2f; + if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold) { - Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); + string msgId = "DialogLowOxygen"; + Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - if (Character.Bleeding > 2.0f) + if (Character.Bleeding > 2.0f && !Character.IsMedic) { - Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); + string msgId = "DialogBleeding"; + Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) + if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure) { - Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); + if (Character.PressureProtection > 0) + { + string msgId = "DialogInsufficientPressureProtection"; + Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); + } + else if (Character.CurrentHull?.DisplayName != null) + { + string msgId = "DialogPressure"; + Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); + } } } @@ -1205,7 +1290,7 @@ namespace Barotrauma bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { - if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) + if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)) { AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } @@ -1371,7 +1456,7 @@ namespace Barotrauma } else { - if (humanAI.ObjectiveManager.GetActiveObjective()?.Enemy == attacker) + if (humanAI.ObjectiveManager.GetLastActiveObjective()?.Enemy == attacker) { // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; @@ -1556,7 +1641,7 @@ namespace Barotrauma hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.IsProtectedFromPressure; + needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure; return needsAir || needsSuit; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) @@ -1656,7 +1741,7 @@ namespace Barotrauma if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage); } if (accumulatedDamage <= WarningThreshold) { return; } @@ -1812,7 +1897,7 @@ namespace Barotrauma /// public static void PropagateHullSafety(Character character, Hull hull) { - DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull)); + DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull)); } private void RefreshHullSafety(Hull hull) @@ -1885,7 +1970,7 @@ namespace Barotrauma private static bool AddTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { bool targetAdded = false; - DoForEachCrewMember(caller, humanAI => + DoForEachBot(caller, humanAI => { if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } var objective = humanAI.ObjectiveManager.GetObjective(); @@ -1902,7 +1987,7 @@ namespace Barotrauma public static void RemoveTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { - DoForEachCrewMember(caller, humanAI => + DoForEachBot(caller, humanAI => humanAI.ObjectiveManager.GetObjective()?.ReportedTargets.Remove(target)); } @@ -2024,6 +2109,10 @@ namespace Barotrauma public float GetHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { + if (hull == null) + { + return CalculateHullSafety(hull, character, visibleHulls); + } if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety)) { hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls)); @@ -2038,6 +2127,10 @@ namespace Barotrauma public static float GetHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { + if (hull == null) + { + return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + } HullSafety hullSafety; if (character.AIController is HumanAIController controller) { @@ -2069,103 +2162,137 @@ namespace Barotrauma if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. - if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + return sameTeam || me.TeamID != CharacterTeamType.None; } else { if (!me.IsSameSpeciesOrGroup(other)) { return false; } } - if (GameMain.GameSession?.GameMode is CampaignMode campaign) + if (GameMain.GameSession?.GameMode is CampaignMode) { - if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || + if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) { Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; - Identifier npcFaction = npc.Faction; - Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; - if (npcFaction.IsEmpty) + + //NPCs that allow some campaign interaction are not turned hostile by low reputation + if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } + + if (npc.AIController is HumanAIController npcAI) { - //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost - npcFaction = currentLocationFaction; - } - if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) - { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) - { - return false; - } + return !npcAI.IsInHostileFaction(); } } } return true; } - public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; - - public static bool IsTrueForAllCrewMembers(Character character, Func predicate) + public bool IsInHostileFaction() { - if (character == null) { return false; } - foreach (var c in Character.CharacterList) + if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; } + if (Character.IsEscorted) { return false; } + + Identifier npcFaction = Character.Faction; + Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; + + if (npcFaction.IsEmpty) { - if (FilterCrewMember(character, c)) - { - if (!predicate(c.AIController as HumanAIController)) - { - return false; - } - } + //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost + npcFaction = currentLocationFaction; } - return true; - } - - public static bool IsTrueForAnyCrewMember(Character character, Func predicate) - { - if (character == null) { return false; } - foreach (var c in Character.CharacterList) + if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) { - if (FilterCrewMember(character, c)) + if (campaign.CurrentLocation is { IsFactionHostile: true }) { - if (predicate(c.AIController as HumanAIController)) - { - return true; - } + return true; } } return false; } - public static int CountCrew(Character character, Func predicate = null, bool onlyActive = true, bool onlyBots = false) + public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious; + + public static bool IsTrueForAllBotsInTheCrew(Character character, Func predicate) + { + if (character == null) { return false; } + foreach (var c in Character.CharacterList) + { + if (!IsBotInTheCrew(character, c)) { continue; } + if (!predicate(c.AIController as HumanAIController)) + { + return false; + } + } + return true; + } + + public static bool IsTrueForAnyBotInTheCrew(Character character, Func predicate) + { + if (character == null) { return false; } + foreach (var c in Character.CharacterList) + { + if (!IsBotInTheCrew(character, c)) { continue; } + if (predicate(c.AIController as HumanAIController)) + { + return true; + } + } + return false; + } + + public static int CountBotsInTheCrew(Character character, Func predicate = null) { if (character == null) { return 0; } int count = 0; foreach (var other in Character.CharacterList) { - if (onlyActive && !IsActive(other)) + if (!IsBotInTheCrew(character, other)) { continue; } + if (predicate == null || predicate(other.AIController as HumanAIController)) { - continue; - } - if (onlyBots && other.IsPlayer) - { - continue; - } - if (FilterCrewMember(character, other)) - { - if (predicate == null || predicate(other.AIController as HumanAIController)) - { - count++; - } + count++; } } return count; } - public static void DoForEachCrewMember(Character character, Action action, float range = float.PositiveInfinity) + /// + /// Including the player characters in the same team. + /// + public bool IsTrueForAnyCrewMember(Func predicate, bool onlyActive = true, bool onlyConnectedSubs = false) + { + foreach (var c in Character.CharacterList) + { + if (!IsActive(c)) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (onlyActive && c.IsIncapacitated) { continue; } + if (onlyConnectedSubs) + { + if (Character.Submarine == null) + { + if (c.Submarine != null) + { + return false; + } + } + else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine)) + { + return false; + } + } + if (predicate(c)) + { + return true; + } + } + return false; + } + + private static void DoForEachBot(Character character, Action action, float range = float.PositiveInfinity) { if (character == null) { return; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) + if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range)) { action(c.AIController as HumanAIController); } @@ -2185,7 +2312,7 @@ namespace Barotrauma } } - private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); + private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController; public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) { @@ -2230,10 +2357,9 @@ namespace Barotrauma bool isOrder = IsOrderedToOperateThis(Character.AIController); foreach (Character c in Character.CharacterList) { + if (!IsActive(c)) { continue; } if (c == Character) { continue; } - if (c.Removed) { continue; } if (c.TeamID != Character.TeamID) { continue; } - if (c.IsIncapacitated) { continue; } if (c.IsPlayer) { if (c.SelectedItem == target.Item) @@ -2301,9 +2427,9 @@ namespace Barotrauma bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); foreach (var c in Character.CharacterList) { + if (!IsActive(c)) { continue; } if (c == Character) { continue; } if (c.TeamID != Character.TeamID) { continue; } - if (c.IsIncapacitated) { continue; } other = c; if (c.IsPlayer) { @@ -2317,7 +2443,7 @@ namespace Barotrauma { var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective(); if (repairItemsObjective == null) { continue; } - if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target) + if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target) { // Not targeting the same item. continue; @@ -2352,11 +2478,10 @@ namespace Barotrauma } #region Wrappers - public bool IsFriendly(Character other) => IsFriendly(Character, other); - public void DoForEachCrewMember(Action action) => DoForEachCrewMember(Character, action); - public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); - public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); - public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); + public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam); + public bool IsTrueForAnyBotInTheCrew(Func predicate) => IsTrueForAnyBotInTheCrew(Character, predicate); + public bool IsTrueForAllBotsInTheCrew(Func predicate) => IsTrueForAllBotsInTheCrew(Character, predicate); + public int CountBotsInTheCrew(Func predicate = null) => CountBotsInTheCrew(Character, predicate); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index a98233d5d..0540b4243 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -180,7 +180,7 @@ namespace Barotrauma private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { - bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished; + bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null; if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) { Vector2 targetDiff = target - currentTarget; @@ -205,6 +205,24 @@ namespace Barotrauma if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; + if (!needsNewPath && findPathTimer < -1) + { + if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0) + { + // Not moving -> need a new path. + needsNewPath = true; + } + if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null) + { + // Current node inside, while we are outside + // -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile) + float maxDist = 200; + if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist) + { + needsNewPath = true; + } + } + } if (findPathTimer < 0) { SkipCurrentPathNodes(); @@ -213,7 +231,7 @@ namespace Barotrauma pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); - bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; + bool useNewPath = needsNewPath; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). @@ -387,49 +405,57 @@ namespace Barotrauma } if (character.IsClimbing && useLadders) { - bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; - if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) + if (currentLadder == null && nextLadder != null) { - //climbing ladders -> don't move horizontally - diff.X = 0.0f; + // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. + NextNode(!doorsChecked); } - //at the same height as the waypoint - float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; - if (heightDiff < colliderSize) + else { - float heightFromFloor = character.AnimController.GetHeightFromFloor(); - // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. - bool isAboveFloor = heightFromFloor > -0.1f; - // If the next waypoint is horizontally far, we don't want to keep holding the ladders - if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) + bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; + if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) { - character.StopClimbing(); + //climbing ladders -> don't move horizontally + diff.X = 0.0f; } - else if (nextLadder != null && !nextLadderSameAsCurrent) + //at the same height as the waypoint + float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); + float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; + if (heightDiff < colliderSize) { - // Try to change the ladder (hatches between two submarines) - if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) + float heightFromFloor = character.AnimController.GetHeightFromFloor(); + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. + bool isAboveFloor = heightFromFloor > -0.1f; + // If the next waypoint is horizontally far, we don't want to keep holding the ladders + if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) { - if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) + character.StopClimbing(); + } + else if (nextLadder != null && !nextLadderSameAsCurrent) + { + // Try to change the ladder (hatches between two submarines) + if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) { - NextNode(!doorsChecked); + if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) + { + NextNode(!doorsChecked); + } } } + if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10) + { + NextNode(!doorsChecked); + } } - if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)) + else if (nextLadder != null) { - NextNode(!doorsChecked); - } - } - else if (nextLadder != null) - { - //if the current node is below the character and the next one is above (or vice versa) - //and both are on ladders, we can skip directly to the next one - //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above - if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) - { - NextNode(!doorsChecked); + if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) + { + //if the current node is below the character and the next one is above (or vice versa) + //and both are on ladders, we can skip directly to the next one + //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above + NextNode(!doorsChecked); + } } } return ConvertUnits.ToSimUnits(diff); @@ -486,7 +512,7 @@ namespace Barotrauma } } float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed)) + if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed)) { NextNode(!doorsChecked); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 1c6a1df7e..2fb0e0027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -122,7 +122,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) + if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 3a16cf84d..dc9946531 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -506,15 +506,29 @@ namespace Barotrauma } } - protected static bool CanEquip(Character character, Item item) + protected static bool CanEquip(Character character, Item item, bool allowWearing) { - bool canEquip = item != null; - if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any)) + if (item == null) { return false; } + bool canEquip = false; + if (item.AllowedSlots.Contains(InvSlotType.Any)) + { + if (character.Inventory.IsAnySlotAvailable(item)) + { + canEquip = true; + } + } + if (!canEquip) { - canEquip = false; var inv = character.Inventory; foreach (var allowedSlot in item.AllowedSlots) { + if (!allowWearing) + { + if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand)) + { + continue; + } + } foreach (var slotType in inv.SlotTypes) { if (!allowedSlot.HasFlag(slotType)) { continue; } @@ -530,18 +544,9 @@ namespace Barotrauma } } } - return canEquip; - } - protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet identifiersOrTags) - { - if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; } - foreach (var identifier in identifiersOrTags) - { - if (item.HasTag(identifier)) { return true; } - } - return false; + return canEquip && character.Inventory.CanBePut(item); } - protected bool CanEquip(Item item) => CanEquip(character, item); + protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index dfc2efe6d..d9c0c85ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -64,7 +64,6 @@ namespace Barotrauma if (subObjectives.Any()) { return; } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { - itemIndex = 0; if (suitableContainer != null) { bool equip = item.GetComponent() != null || @@ -112,10 +111,7 @@ namespace Barotrauma Abandon = true; } } - else - { - objectiveManager.GetObjective().Wander(deltaTime); - } + objectiveManager.GetObjective().Wander(deltaTime); } protected override bool CheckObjectiveSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 1b2f390e2..c7bbf9574 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -121,7 +121,7 @@ namespace Barotrauma { return true; } - return CanEquip(character, item); + return CanEquip(character, item, allowWearing: false); } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 066b2b9a1..c0ae5381b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -170,13 +170,33 @@ namespace Barotrauma return Priority; } } - float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f); - Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100); - if (Priority > 0) + if (TargetEliminated) { - if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) + Priority = 0; + } + else + { + // 91-100 + float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; + float maxPriority = AIObjectiveManager.MaxObjectivePriority; + float priorityScale = maxPriority - minPriority; + float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) { - Priority = 0; + xDist /= 2; + yDist /= 2; + } + float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5); + float devotion = CumulatedDevotion / 100; + float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1)); + Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority); + if (Priority > 0) + { + if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) + { + Priority = 0; + } } } return Priority; @@ -312,12 +332,10 @@ namespace Barotrauma } else { - AskHelp(); Retreat(deltaTime); } break; case CombatMode.Retreat: - AskHelp(); Retreat(deltaTime); break; default: @@ -352,7 +370,7 @@ namespace Barotrauma Weapon = null; continue; } - if (WeaponComponent.IsNotEmpty(character)) + if (!WeaponComponent.IsEmpty(character)) { // All good, the weapon is loaded break; @@ -470,7 +488,7 @@ namespace Barotrauma // Not in the inventory anymore or cannot find the weapon component return false; } - if (!WeaponComponent.IsNotEmpty(character)) + if (WeaponComponent.IsEmpty(character)) { // Try reloading (and seek ammo) if (!Reload(seekAmmo)) @@ -541,7 +559,7 @@ namespace Barotrauma priority /= 2; } } - if (!weapon.IsNotEmpty(character)) + if (weapon.IsEmpty(character)) { if (weapon is RangedWeapon && !isAllowedToSeekWeapons) { @@ -554,7 +572,6 @@ namespace Barotrauma priority /= 2; } } - if (Enemy.Params.Health.StunImmunity) { if (weapon.Item.HasTag("stunner")) @@ -750,7 +767,7 @@ namespace Barotrauma private bool Equip() { if (character.LockHands) { return false; } - if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) + if (WeaponComponent.IsEmpty(character)) { return false; } @@ -783,6 +800,10 @@ namespace Barotrauma private void Retreat(float deltaTime) { + if (!Enemy.IsHuman) + { + SpeakRetreating(); + } RemoveFollowTarget(); RemoveSubObjective(ref seekAmmunitionObjective); if (retreatObjective != null && retreatObjective.Target != retreatTarget) @@ -793,6 +814,7 @@ namespace Barotrauma { // Swim away SteeringManager.Reset(); + character.ReleaseSecondaryItem(); SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition)); SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); return; @@ -819,7 +841,8 @@ namespace Barotrauma { TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager) { - UsePathingOutside = false + UsePathingOutside = false, + SpeakIfFails = false }, onAbandon: () => { @@ -861,6 +884,7 @@ namespace Barotrauma { if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range)) { + character.ReleaseSecondaryItem(); // Swim towards the target SteeringManager.Reset(); SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10); @@ -882,7 +906,8 @@ namespace Barotrauma UsePathingOutside = false, IgnoreIfTargetDead = true, TargetName = Enemy.DisplayName, - AlwaysUseEuclideanDistance = false + AlwaysUseEuclideanDistance = false, + SpeakIfFails = false }, onAbandon: () => { @@ -966,7 +991,7 @@ namespace Barotrauma item.GetComponent() != null) { item.Drop(character); - character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot); + character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot); } } } @@ -1028,54 +1053,43 @@ namespace Barotrauma if (Weapon.OwnInventory == null) { return true; } // Eject empty ammo HumanAIController.UnequipEmptyItems(Weapon); - RelatedItem item = null; - Item ammunition = null; ImmutableHashSet ammunitionIdentifiers = null; if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) { - ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); - if (ammunition != null) - { - // Ammunition still remaining - return true; - } - item = requiredItem; + if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; } ammunitionIdentifiers = requiredItem.Identifiers; + break; } } else if (WeaponComponent is MeleeWeapon meleeWeapon) { ammunitionIdentifiers = meleeWeapon.PreferredContainedItems; } - // No ammo - if (ammunition == null) + if (ammunitionIdentifiers != null) { - if (ammunitionIdentifiers != null) + // Try reload ammunition from inventory + static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); + if (ammunition != null) { - // Try reload ammunition from inventory - static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); - ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); - if (ammunition != null) + var container = Weapon.GetComponent(); + if (!container.Inventory.TryPutItem(ammunition, user: character)) { - var container = Weapon.GetComponent(); - if (!container.Inventory.TryPutItem(ammunition, null)) + if (ammunition.ParentInventory == character.Inventory) { - if (ammunition.ParentInventory == character.Inventory) - { - ammunition.Drop(character); - } + ammunition.Drop(character); } } } } - if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) + if (!WeaponComponent.IsEmpty(character)) { return true; } - else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) + else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) { SeekAmmunition(ammunitionIdentifiers); } @@ -1270,7 +1284,7 @@ namespace Barotrauma } private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); + private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); private void Speak(Identifier textIdentifier, float delay, float minDuration) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 4c1d874d2..c39168a42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -109,7 +109,7 @@ namespace Barotrauma private bool CheckItem(Item item) { - return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); + return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); } protected override void Act(float deltaTime) @@ -156,15 +156,15 @@ namespace Barotrauma Inventory originalInventory = ItemToContain.ParentInventory; var slots = originalInventory?.FindIndices(ItemToContain); - static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) + bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) { if (targetSlot.HasValue) { - return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null); + return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character); } else { - return inventory.TryPutItem(itemToContain, user: null); + return inventory.TryPutItem(itemToContain, user: character); } } @@ -202,7 +202,7 @@ namespace Barotrauma ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder(), - endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index aee20f6ef..cec282b90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -66,7 +66,7 @@ namespace Barotrauma else { float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } } return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 3c44882bf..a81367582 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -28,7 +28,7 @@ namespace Barotrauma if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. - return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; + return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100; } protected override AIObjective ObjectiveConstructor(Character target) @@ -37,8 +37,7 @@ namespace Barotrauma var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + if (campaign.CurrentLocation is { IsFactionHostile: true }) { combatObjective.holdFireCondition = () => { @@ -66,7 +65,13 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { return false; } + if (!targetCharactersInOtherSubs) + { + if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID) + { + return false; + } + } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index a87e5cd61..aa10e388d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -79,7 +79,7 @@ namespace Barotrauma { if (mask != targetItem) { - character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot); + character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 221067c28..97e56d4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -52,17 +52,14 @@ namespace Barotrauma objectiveManager.HasOrder(o => o.Priority > 0) || objectiveManager.HasActiveObjective() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; + && ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10; } else { if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || - (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && - (needsSuit ? - !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : - !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))) + NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) { - Priority = 100; + Priority = AIObjectiveManager.MaxObjectivePriority; } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) @@ -75,11 +72,11 @@ namespace Barotrauma { Priority = 0; } - Priority = MathHelper.Clamp(Priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority); if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { // Boost the priority while seeking the diving gear - Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100)); + Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority)); } } return Priority; @@ -111,7 +108,7 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; - if (currenthullSafety >= 100) + if (currenthullSafety >= 100 && !character.IsLowInOxygen) { // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. Priority = 0; @@ -122,7 +119,7 @@ namespace Barotrauma float dangerFactor = (100 - currenthullSafety) / 100; Priority += dangerFactor * priorityIncrease * deltaTime; } - Priority = MathHelper.Clamp(Priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority); } } @@ -138,7 +135,7 @@ namespace Barotrauma { if (resetPriority) { return; } var currentHull = character.CurrentHull; - bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); + bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure; bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { @@ -200,16 +197,11 @@ namespace Barotrauma UpdateSimpleEscape(deltaTime); return; } - searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; currentSafeHull = potentialSafeHull; - - cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); - if (currentSafeHull == null) - { - currentSafeHull = previousSafeHull; - } + cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull); + currentSafeHull ??= previousSafeHull; if (currentSafeHull != null && currentSafeHull != currentHull) { if (goToObjective?.Target != currentSafeHull) @@ -219,6 +211,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { + SpeakIfFails = false, AllowGoingOutside = character.IsProtectedFromPressure || character.CurrentHull == null || @@ -300,6 +293,7 @@ namespace Barotrauma //only move if we haven't reached the edge of the room if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) { + character.ReleaseSecondaryItem(); character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); } else @@ -349,7 +343,6 @@ namespace Barotrauma if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; } - //sort the hulls based on distance and which sub they're in //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive @@ -493,5 +486,18 @@ namespace Barotrauma cannotFindDivingGear = false; cannotFindSafeHull = false; } + + private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0) + { + if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; } + if (needsSuit) + { + return !HumanAIController.HasDivingSuit(character, minOxygen); + } + else + { + return !HumanAIController.HasDivingGear(character, minOxygen); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index dd0b1e20b..f08a264c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -37,40 +37,56 @@ namespace Barotrauma { Priority = 0; Abandon = true; + return Priority; } - else if (HumanAIController.IsTrueForAnyCrewMember( - other => other != HumanAIController && - other.Character.IsBot && - other.ObjectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && - fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak))) + float coopMultiplier = 1; + foreach (var c in Character.CharacterList) { - Priority = 0; + if (!HumanAIController.IsActive(c)) { continue; } + if (c.TeamID != character.TeamID) { continue; } + if (c == character) { continue; } + if (c.IsPlayer) { continue; } + if (c.AIController is HumanAIController otherAI ) + { + if (otherAI.ObjectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeak fixLeak) + { + if (fixLeak.Leak == Leak) + { + // Ignore leaks that others are already targeting + Priority = 0; + return Priority; + } + if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull) + { + // Reduce the priority of leaks that others should be targeting + coopMultiplier = 0.1f; + break; + } + } + } + } + float reduction = isPriority ? 1 : 2; + float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; + if (operateObjective != null && objectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + { + // Prioritize leaks that we are already fixing + Priority = maxPriority; } else { - float reduction = isPriority ? 1 : 2; - float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; - if (operateObjective != null && objectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); + if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) { - // Prioritize leaks that we are already fixing - Priority = maxPriority; - } - else - { - float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). - // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); - if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) - { - // Double the distance when the leak can be accessed from the current hull. - distanceFactor *= 2; - } - float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + // Double the distance when the leak can be accessed from the current hull. + distanceFactor *= 2; } + float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1)); } return Priority; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 9094affbe..d9c8ff24a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -38,9 +38,9 @@ namespace Barotrauma protected override float TargetEvaluation() { - int totalLeaks = Targets.Count(); + int totalLeaks = Targets.Count; if (totalLeaks == 0) { return 0; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true); + int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && c.Character.Submarine == character.Submarine); bool anyFixers = otherFixers > 0; if (objectiveManager.IsOrder(this)) { @@ -52,7 +52,7 @@ namespace Barotrauma int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); int leaks = totalLeaks - secondaryLeaks; float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1; - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 2f3e75a45..a8968f731 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -1,9 +1,11 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Immutable; using System.Collections.Generic; using System.Linq; +using System.Diagnostics; namespace Barotrauma { @@ -31,14 +33,15 @@ namespace Barotrauma private ISpatialEntity moveToTarget; private bool isDoneSeeking; public Item TargetItem => targetItem; - private int currSearchIndex; + private int currentSearchIndex; public ImmutableHashSet ignoredContainerIdentifiers; public ImmutableHashSet ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; - public static float DefaultReach = 100; + public const float DefaultReach = 100; + public const float MaxReach = 150; public bool AllowToFindDivingGear { get; set; } = true; public bool MustBeSpecificItem { get; set; } @@ -76,7 +79,7 @@ namespace Barotrauma public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - currSearchIndex = -1; + currentSearchIndex = 0; Equip = equip; originalTarget = targetItem; this.targetItem = targetItem; @@ -89,7 +92,7 @@ namespace Barotrauma public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { - currSearchIndex = -1; + currentSearchIndex = 0; Equip = equip; this.spawnItemIfNotFound = spawnItemIfNotFound; this.checkInventory = checkInventory; @@ -125,7 +128,7 @@ namespace Barotrauma public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) { - return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach); } private bool CheckInventory() @@ -246,7 +249,7 @@ namespace Barotrauma else { character.SelectCharacter(c); - canInteract = character.CanInteractWith(c, maxDist: DefaultReach); + canInteract = character.CanInteractWith(c); character.DeselectCharacter(); } } @@ -268,7 +271,7 @@ namespace Barotrauma Inventory itemInventory = targetItem.ParentInventory; var slots = itemInventory?.FindIndices(targetItem); - if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true)) + if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags)) { if (TakeWholeStack && slots != null) { @@ -298,8 +301,11 @@ namespace Barotrauma if (!Equip) { // Try equipping and wearing the item - Wear = true; Equip = true; + if (!objectiveManager.HasActiveObjective() && !objectiveManager.HasActiveObjective()) + { + Wear = true; + } return; } #if DEBUG @@ -342,6 +348,10 @@ namespace Barotrauma } } + private Stopwatch sw; + private Stopwatch StopWatch => sw ??= new Stopwatch(); + private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>(); + private List itemList; private void FindTargetItem() { if (IdentifiersOrTags == null) @@ -349,13 +359,16 @@ namespace Barotrauma if (targetItem == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red); + DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined."); #endif Abandon = true; } return; } - + if (HumanAIController.DebugAI) + { + StopWatch.Restart(); + } float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); if (!CheckPathForEachItem) { @@ -366,12 +379,22 @@ namespace Barotrauma CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective); } bool checkPath = CheckPathForEachItem; - bool hasCalledPathFinder = false; - int itemsPerFrame = (int)priority; - for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++) + // Reset if the character has switched subs. + if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true)) { - currSearchIndex++; - var item = Item.ItemList[currSearchIndex]; + currentSearchIndex = 0; + } + if (currentSearchIndex == 0) + { + itemCandidates.Clear(); + itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true); + } + int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority)); + int checkedItems = 0; + for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++) + { + checkedItems++; + var item = itemList[currentSearchIndex]; Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; if (itemSub == null) { continue; } Submarine mySub = character.Submarine; @@ -395,8 +418,6 @@ namespace Barotrauma if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } } - // Don't allow going into another sub, unless it's connected and of the same team and type. - if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; } if (character.IsItemTakenBySomeoneElse(item)) { continue; } if (item.ParentInventory is ItemInventory itemInventory) { @@ -411,11 +432,14 @@ namespace Barotrauma if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(character)) { continue; } - if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } - //the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool) - if (ownerItem != item.Container) + if (ownerItem != item) { - itemPriority *= 0.1f; + if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } + //the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool) + if (ownerItem != item.Container) + { + itemPriority *= 0.1f; + } } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; @@ -463,22 +487,69 @@ namespace Barotrauma { itemPriority *= item.Condition / item.MaxCondition; } + if (checkPath) + { + itemCandidates.Add((item, itemPriority)); + } // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } - if (!hasCalledPathFinder && PathSteering != null && checkPath) + if (EvaluateCombatPriority && itemPriority <= 0) { - hasCalledPathFinder = true; - var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) { continue; } + // Not good enough + continue; } currItemPriority = itemPriority; targetItem = item; moveToTarget = rootInventoryOwner ?? item; } - if (currSearchIndex >= Item.ItemList.Count - 1) + if (currentSearchIndex >= itemList.Count - 1) { isDoneSeeking = true; - if (targetItem == null) + } + if (checkedItems > 0) + { + if (isDoneSeeking && itemCandidates.Any()) + { + itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority)); + } + if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2) + { + var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; + if (StopWatch.ElapsedMilliseconds > 5) + { + DebugConsole.ThrowError(msg); + } + else + { + // An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue. + DebugConsole.AddWarning(msg); + } + } + } + if (isDoneSeeking) + { + if (PathSteering == null) + { + itemCandidates.Clear(); + } + if (itemCandidates.Any()) + { + if (itemCandidates.FirstOrDefault() is { } itemCandidate) + { + var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) + { + // Remove the invalid candidates and continue on the next frame. + itemCandidates.Remove(itemCandidate); + } + else + { + // The path was valid -> we are done. + itemCandidates.Clear(); + } + } + } + if (targetItem == null && itemCandidates.None()) { if (spawnItemIfNotFound) { @@ -569,11 +640,11 @@ namespace Barotrauma { if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; - if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } + if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } - if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; } - return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; } + return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); } public override void Reset() @@ -591,7 +662,7 @@ namespace Barotrauma targetItem = originalTarget; moveToTarget = targetItem?.GetRootInventoryOwner(); isDoneSeeking = false; - currSearchIndex = 0; + currentSearchIndex = 0; currItemPriority = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index e4afa8c54..521053319 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -257,7 +257,7 @@ namespace Barotrauma } } } - else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false)) + else if (HumanAIController.HasValidPath(requireUnfinished: false)) { waitUntilPathUnreachable = pathWaitingTime; } @@ -364,7 +364,8 @@ namespace Barotrauma else { bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null; - if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true)) + bool isEitherOneInside = isInside || Target.Submarine != null; + if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath())) { SeekGaps(maxGapDistance); seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); @@ -388,6 +389,10 @@ namespace Barotrauma } } } + else + { + TargetGap = null; + } } } else @@ -436,9 +441,9 @@ namespace Barotrauma { var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); - bool handsFull = - (leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) || - (rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1); + bool handsFull = + (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) || + (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem)); if (!handsFull) { bool hasBattery = false; @@ -473,7 +478,7 @@ namespace Barotrauma // Try to switch batteries if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) { - scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot)); + scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot)); if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) { useScooter = false; @@ -488,7 +493,7 @@ namespace Barotrauma if (!useScooter) { // Unequip - character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot); + character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot); } } } @@ -511,13 +516,20 @@ namespace Barotrauma { nodeFilter = n => n.Waypoint.CurrentHull != null; } - else if (!isInside && HumanAIController.UseIndoorSteeringOutside) + else if (!isInside) { - nodeFilter = n => n.Waypoint.Submarine == null; + if (HumanAIController.UseOutsideWaypoints) + { + nodeFilter = n => n.Waypoint.Submarine == null; + } + else + { + nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null; + } } - if (!isInside && !UsePathingOutside) { + character.ReleaseSecondaryItem(); PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10); if (character.AnimController.InWater) { @@ -540,6 +552,7 @@ namespace Barotrauma } else { + character.ReleaseSecondaryItem(); SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); if (character.AnimController.InWater) { @@ -560,6 +573,7 @@ namespace Barotrauma } else { + character.ReleaseSecondaryItem(); SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); if (character.AnimController.InWater) { @@ -573,6 +587,7 @@ namespace Barotrauma { if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; } SteeringManager.Reset(); + character.ReleaseSecondaryItem(); character.CursorPosition = targetWorldPos; if (character.Submarine != null) { @@ -624,7 +639,7 @@ namespace Barotrauma } else if (target is Character c) { - return c.CurrentHull; + return c.CurrentHull ?? c.AnimController.CurrentHull; } else if (target is Structure structure) { @@ -772,6 +787,14 @@ namespace Barotrauma { StopMovement(); HumanAIController.FaceTarget(Target); + if (Target is WayPoint { Ladders: null }) + { + // Release ladders when ordered to wait at a spawnpoint. + // This is a special case specifically meant for NPCs that spawn in outposts with a wait order. + // Otherwise they might keep holding to the ladders when the target is just next to it. + // Releasing too early should be handled inside the IsCloseEnough property. + character.ReleaseSecondaryItem(); + } base.OnCompleted(); } @@ -781,6 +804,10 @@ namespace Barotrauma findDivingGear = null; seekGapsTimer = 0; TargetGap = null; + if (SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 3625f7a1e..0cb63b791 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -163,15 +163,22 @@ namespace Barotrauma character.SelectedItem = null; - CleanupItems(deltaTime); + if (!character.IsClimbing) + { + CleanupItems(deltaTime); + } if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null) { TargetHull = character.CurrentHull; } - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) + bool currentTargetIsInvalid = + currentTarget == null || + IsForbidden(currentTarget) || + (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + + if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull)) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -305,12 +312,8 @@ namespace Barotrauma { if (character.IsClimbing) { - if (character.AnimController.GetHeightFromFloor() < 0.1f) - { - character.AnimController.Anim = AnimController.Animation.None; - character.SelectedSecondaryItem = null; - } - return; + PathSteering.Reset(); + character.StopClimbing(); } var currentHull = character.CurrentHull; if (!character.AnimController.InWater && currentHull != null) @@ -362,6 +365,7 @@ namespace Barotrauma } else { + character.ReleaseSecondaryItem(); PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff)); } return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index b8c94aca0..c3ec57136 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -24,7 +24,7 @@ namespace Barotrauma private ImmutableHashSet ValidContainableItemIdentifiers { get; } private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); - private int itemIndex = 0; + private int itemIndex; private AIObjectiveDecontainItem decontainObjective; private readonly HashSet ignoredItems = new HashSet(); private Item targetItem; @@ -219,9 +219,8 @@ namespace Barotrauma return Priority; } - public override void Update(float deltaTime) + protected override void Act(float deltaTime) { - base.Update(deltaTime); if (targetItem == null) { if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) @@ -233,6 +232,7 @@ namespace Barotrauma } targetItem = item; } + objectiveManager.GetObjective().Wander(deltaTime); float GetPriority(Item item) { try @@ -256,11 +256,7 @@ namespace Barotrauma } } } - } - - protected override void Act(float deltaTime) - { - if (targetItem != null) + else { if(decontainObjective == null && !IsValidContainable(targetItem)) { @@ -290,10 +286,6 @@ namespace Barotrauma Reset(); }); } - else - { - objectiveManager.GetObjective().Wander(deltaTime); - } } private bool IsValidContainable(Item item) @@ -310,7 +302,7 @@ namespace Barotrauma if (parentItem.HasTag("donttakeitems")) { return false; } } if (!item.HasAccess(character)) { return false; } - if (!character.HasItem(item) && !CanEquip(item)) { return false; } + if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index c54980f2e..eeee31b23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -63,7 +63,7 @@ namespace Barotrauma { if (item == null || item.Removed) { return false; } if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } - if (!(item.GetComponent() is ItemContainer container)) { return false; } + if ((item.GetComponent() is not ItemContainer container)) { return false; } if (container.Inventory == null) { return false; } if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 38aa4b5d5..49ab6d61f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -89,12 +89,7 @@ namespace Barotrauma var target = objective.Key; if (!Targets.Contains(target)) { - var subObjective = objective.Value; - if (CurrentSubObjective == subObjective) - { - CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted; - } - subObjectives.Remove(subObjective); + subObjectives.Remove(objective.Value); } } SyncRemovedObjectives(Objectives, GetList()); @@ -157,6 +152,11 @@ namespace Barotrauma else { float max = AIObjectiveManager.LowestOrderPriority - 1; + if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) + { + // Allow higher prio + max = AIObjectiveManager.EmergencyObjectivePriority; + } float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); Priority = MathHelper.Lerp(0, max, value); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 142a57783..2c83b3ef7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma MaxValue = 2 } + public const float MaxObjectivePriority = 100; + public const float EmergencyObjectivePriority = 90; public const float HighestOrderPriority = 70; public const float LowestOrderPriority = 60; public const float RunPriority = 50; @@ -125,11 +127,21 @@ namespace Barotrauma { CoroutineManager.StopCoroutines(delayedObjective.Value); } + + var prevIdleObjective = GetObjective(); + DelayedObjectives.Clear(); Objectives.Clear(); FailedAutonomousObjectives = false; AddObjective(new AIObjectiveFindSafety(character, this)); - AddObjective(new AIObjectiveIdle(character, this)); + var newIdleObjective = new AIObjectiveIdle(character, this); + if (prevIdleObjective != null) + { + newIdleObjective.TargetHull = prevIdleObjective.TargetHull; + newIdleObjective.Behavior = prevIdleObjective.Behavior; + prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t)); + } + AddObjective(newIdleObjective); int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { @@ -155,7 +167,7 @@ namespace Barotrauma AddObjective(objective, delay: Rand.Value() / 2); objectiveCount++; } - } + } _waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount); } @@ -410,7 +422,7 @@ namespace Barotrauma newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { CloseEnough = Rand.Range(80f, 100f), - CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), + CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, @@ -633,10 +645,11 @@ namespace Barotrauma public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; - /// - /// Returns the last active objective of the specific type. - /// - public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + public T GetLastActiveObjective() where T : AIObjective + => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + + public T GetFirstActiveObjective() where T : AIObjective + => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T; /// /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 06305e134..163b7bee2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -122,7 +122,7 @@ namespace Barotrauma else if (!isOrder) { var steering = component?.Item.GetComponent(); - if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain))) + if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true))) { // Ignore if already set to autopilot or if there's a captain onboard Priority = 0; @@ -204,7 +204,7 @@ namespace Barotrauma } if (operateTarget != null) { - if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) + if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) { // Another crew member is already targeting this entity (leak). Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index de55f4035..b46f1f2e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -69,16 +69,6 @@ namespace Barotrauma if (subObjective != null && subObjective.IsCompleted) { Priority = 0; - items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character)); - if (items.None()) - { - Abandon = true; - - } - else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character)))) - { - Reset(); - } } return Priority; } @@ -162,25 +152,25 @@ namespace Barotrauma }; } if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, - onCompleted: () => - { - if (KeepActiveWhenReady) + onCompleted: () => { - if (getSingleItemObjective != null) + if (KeepActiveWhenReady) { - var item = getSingleItemObjective?.TargetItem; - if (item?.IsOwnedBy(character) != null) + if (getSingleItemObjective != null) { - items.Add(item); + var item = getSingleItemObjective?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + items.Add(item); + } } } - } - else - { - IsCompleted = true; - } - }, - onAbandon: () => Abandon = true)) + else + { + IsCompleted = true; + } + }, + onAbandon: () => Abandon = true)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index c2539d899..dbb0a68a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -102,7 +102,7 @@ namespace Barotrauma // Don't stop fixing until completely done return 100; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); + int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective()); int items = Targets.Count; if (items == 0) { @@ -116,7 +116,7 @@ namespace Barotrauma } else { - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index b75a3152e..cba0a97f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -139,14 +139,14 @@ namespace Barotrauma recursive: true); } } - if (character.Submarine != null) + if (character.Submarine != null && targetCharacter.CurrentHull != null) { if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { // Incapacitated target is not in a safe place -> Move to a safe place first if (character.SelectedCharacter != targetCharacter) { - if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", ("[targetname]", targetCharacter.Name, FormatCapitals.No), @@ -293,6 +293,11 @@ namespace Barotrauma currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); + //allow taking items from the target's inventory too if the target is unconscious + if (matchingItem == null && targetCharacter.IsIncapacitated) + { + matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); + } if (matchingItem != null) { bestItem = matchingItem; @@ -379,24 +384,24 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (character != targetCharacter && character.IsOnPlayerTeam) + if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); + SpeakCannotTreat(); } }); } else if (cprSuitability <= 0) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; + SpeakCannotTreat(); } } } else if (!targetCharacter.IsUnconscious) { - //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; + //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) + SpeakCannotTreat(); return; } if (character != targetCharacter) @@ -414,6 +419,14 @@ namespace Barotrauma } } + private void SpeakCannotTreat() + { + LocalizedString msg = character == targetCharacter ? + TextManager.Get("dialogcannottreatself") : + TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No); + character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); + } + private void ApplyTreatment(Affliction affliction, Item item) { item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); @@ -433,50 +446,36 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed) + if (!IsAllowed || targetCharacter == null) { Priority = 0; Abandon = true; return Priority; } - if (character.CurrentHull == null) + if (character.CurrentHull != null) { - if (!objectiveManager.HasOrder()) + if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { + // Don't go into rooms that have enemies Priority = 0; Abandon = true; return Priority; } } - else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); + if (character.Submarine?.Info is { IsRuin: false }) { - // Don't go into rooms that have enemies - Priority = 0; - Abandon = true; - return Priority; + verticalDistance *= 2; } - if (targetCharacter == null) + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); + if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull) { - Priority = 0; - Abandon = true; - } - else - { - float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); - if (character.Submarine?.Info is { IsRuin: false }) - { - verticalDistance *= 2; - } - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); - if (targetCharacter.CurrentHull == character.CurrentHull) - { - distanceFactor = 1; - } - float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); + distanceFactor = 1; } + float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); return Priority; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 50f03a240..6153ee028 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -42,15 +42,16 @@ namespace Barotrauma if (Targets.None()) { return 100; } if (!objectiveManager.IsOrder(this)) { - if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) + if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsMedic, onlyActive: true, onlyConnectedSubs: true)) { - // Don't do anything if there's a medic on board and we are not a medic + // Don't do anything if there's a medic on board actively treating and we are not a medic return 100; } } float worstCondition = Targets.Min(t => GetVitalityFactor(t)); if (Targets.Contains(character)) { + // Targeting self -> higher prio if (character.Bleeding > 10) { // Enforce the highest priority when bleeding out. @@ -117,7 +118,7 @@ namespace Barotrauma { if (!character.IsMedic && target != character) { - // Don't allow to treat others autonomously + // Don't allow to treat others autonomously, unless we are a medic return false; } // Ignore unsafe hulls, unless ordered @@ -136,17 +137,17 @@ namespace Barotrauma // Don't allow going into another sub, unless it's connected and of the same team and type. if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } } - else + else if (target.Submarine != null) { - return target.Submarine == null; + // We are outside, but the target is inside. + return false; } if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { - // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters + // Ignore all concious targets that are currently fighting, fleeing, or treating characters if (targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective() || - targetAI.ObjectiveManager.HasActiveObjective() || - targetAI.ObjectiveManager.HasActiveObjective()) + targetAI.ObjectiveManager.HasActiveObjective()) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 88db60899..338568707 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -228,8 +228,8 @@ namespace Barotrauma { continue; } - //optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one - if (node.TempDistance < 1.0f) + //optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one + if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach)) { if (IsValidStartNode(node)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index b1a83f852..874a081bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -47,7 +47,8 @@ namespace Barotrauma public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option))) + if (OrderedCharacter.AIController is HumanAIController humanAI && + humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem)) { if (orderedCharacter != CommandingCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index d4d1ad1ad..ba29bdbb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +8,7 @@ namespace Barotrauma { class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem { - public override float RedundantIssueModifier => 0.65f; + public override float RedundantIssueModifier => 0.8f; private readonly List targetingImportances = new List(); public override bool AllowEasySwitching => true; @@ -17,30 +18,52 @@ namespace Barotrauma float GetTargetingImportance(Entity entity) { float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition); - return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance); + if (currentDistanceToEnemy > Sonar.DefaultSonarRange) { return 0.0f; } + float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MaxImportance * 0.1f, MaxImportance * 0.5f); + if (TargetItem.Submarine != null && importance > 0.0f) + { + if (TargetItemComponent is Turret turret) + { + if (!turret.CheckTurretAngle(entity.WorldPosition)) + { + importance *= 0.1f; + } + } + else + { + Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition; + Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition; + if (Vector2.Dot(dir, submarineDir) < 0) + { + //direction from the weapon to the target is opposite to the direction from the sub to the weapon + // = the turret is most likely on the wrong side of the sub, reduce importance + importance *= 0.1f; + } + } + + } + return importance; } public override void CalculateImportanceSpecific() { - if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) - { - //operate (= recharge the turrets) with low priority if they're out of power - //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority - Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f; - return; - } - targetingImportances.Clear(); foreach (Character character in shipCommandManager.EnemyCharacters) { targetingImportances.Add(GetTargetingImportance(character)); } // there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing - if (targetingImportances.Any(i => i > 0)) { targetingImportances.Sort(); - Importance = targetingImportances.TakeLast(3).Sum(); + Importance = Math.Max(targetingImportances.TakeLast(3).Sum(), ShipCommandManager.MinimumIssueThreshold); + } + if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) + { + //operate (= recharge the turrets) with low priority if they're out of power + //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority + Importance = Math.Max(ShipCommandManager.MinimumIssueThreshold / RedundantIssueModifier, Importance); + return; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index b4a18d756..2ac885b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -229,7 +229,7 @@ namespace Barotrauma #if DEBUG ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter); #endif - attendedIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, attendedIssues); } else { @@ -237,19 +237,26 @@ namespace Barotrauma ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to"); #endif shipIssueWorker.RemoveOrder(); - availableIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, availableIssues); } } - availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance)); - attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance)); + static void InsertIssue(ShipIssueWorker issue, List list) + { + int index = 0; + while (index < list.Count && list[index].Importance > issue.Importance) + { + index++; + } + list.Insert(index, issue); + } ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault(); float bestValue = 0f; Character bestCharacter = null; - if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold) + if (mostImportantIssue != null && mostImportantIssue.Importance >= MinimumIssueThreshold) { IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index ebc92713c..6d726f848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -480,7 +480,10 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; @@ -717,9 +720,12 @@ namespace Barotrauma { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); - Collider.LinearVelocity = new Vector2( - movement.X, - Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = new Vector2( + movement.X, + Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 37111fefd..dfa3fdba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -841,14 +841,11 @@ namespace Barotrauma if (head == null) { return; } if (torso == null) { return; } - const float DisableMovementAboveSurfaceThreshold = 50.0f; - if (currentHull != null && character.CurrentHull != null) { float surfacePos = GetSurfaceY(); float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos); - if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; } } Limb leftHand = GetLimb(LimbType.LeftHand); @@ -918,6 +915,7 @@ namespace Barotrauma RotateHead(head); } + const float DisableMovementAboveSurfaceThreshold = 50.0f; //dont try to move upwards if head is already out of water if (surfaceLimiter > 1.0f && TargetMovement.Y > 0.0f) { @@ -937,8 +935,8 @@ namespace Barotrauma //turn head above the water head.body.ApplyTorque(Dir); } + movement.Y *= Math.Max(0, 1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); - movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); } bool isNotRemote = true; @@ -957,7 +955,13 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + Vector2 targetVelocity = movement; + //if we're too high above the surface, don't touch the vertical velocity of the collider unless we're heading down + if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) + { + targetVelocity.Y = Math.Min(Collider.LinearVelocity.Y, movement.Y); + }; + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, targetVelocity, t); } WalkPos += movement.Length(); @@ -1255,7 +1259,7 @@ namespace Barotrauma if (ladder.Item.Prefab.Triggers.None()) { - character.SelectedSecondaryItem = null; + character.ReleaseSecondaryItem(); return; } @@ -1525,13 +1529,15 @@ namespace Barotrauma Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } + Limb targetLeftHand = + target.AnimController.GetLimb(LimbType.LeftForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; - Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } + Limb targetRightHand = + target.AnimController.GetLimb(LimbType.RightForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; if (!target.AllowInput) { @@ -1547,10 +1553,7 @@ namespace Barotrauma return; } Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); - if (targetTorso == null) - { - targetTorso = target.AnimController.MainLimb; - } + targetTorso ??= target.AnimController.MainLimb; if (target.AnimController.Dir != Dir) { target.AnimController.Flip(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 01bf6260b..5592b5dd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1299,21 +1299,32 @@ namespace Barotrauma limb.Update(deltaTime); } - if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround) + if (!inWater && character.AllowInput && levitatingCollider) { - float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; - if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround) + if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { - if (Stairs != null) + float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; + if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, - (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); + if (Stairs != null) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, + (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); + } + else + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + } } - else + } + else + { + // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. + if (Collider.LinearVelocity == Vector2.Zero) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + character.IsRagdolled = true; } - } + } } UpdateProjSpecific(deltaTime, cam); forceNotStanding = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 3b4bc3f15..5991498e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -78,6 +78,11 @@ namespace Barotrauma } } + /// + /// Attacks are used to deal damage to characters, structures and items. + /// They can be defined in the weapon components of the items or the limb definitions of the characters. + /// The limb attacks can also be used by the player, when they control a monster or have some appendage, like a husk stinger. + /// partial class Attack : ISerializableEntity { [Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable] @@ -123,7 +128,7 @@ namespace Barotrauma set => _damageRange = value; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float MinRange { get; private set; } [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] @@ -138,22 +143,22 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float CoolDownRandomFactor { get; private set; } = 0; - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable] public bool FullSpeedAfterAttack { get; private set; } private float _structureDamage; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float StructureDamage { get => _structureDamage * DamageMultiplier; set => _structureDamage = value; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable] public bool EmitStructureDamageParticles { get; private set; } private float _itemDamage; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage { get =>_itemDamage * DamageMultiplier; @@ -178,16 +183,16 @@ namespace Barotrauma /// public float ImpactMultiplier { get; set; } = 1; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable] public bool Ranged { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable] + [Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable] public float RequiredAngle { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] @@ -205,16 +210,13 @@ namespace Barotrauma [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] public float SwayFrequency { get; set; } - /// - /// Legacy support. Use Afflictions. - /// - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")] public float Stun { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; set; } - [Serialize("", IsPropertySaveable.Yes), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable] public string ApplyForceOnLimbs { get @@ -240,20 +242,20 @@ namespace Barotrauma [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldStart { get; private set; } - + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldMiddle { get; private set; } - + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldEnd { get; private set; } - - [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable] + + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable] public TransitionMode RootTransitionEasing { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] public float Torque { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable] public bool ApplyForcesOnlyOnce { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] @@ -279,10 +281,10 @@ namespace Barotrauma //public float StickChance { get; set; } public float StickChance => 0f; - [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } - [Serialize(false, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable] public bool Blink { get; private set; } public IEnumerable StatusEffects @@ -298,7 +300,7 @@ namespace Barotrauma private set; } = new Dictionary(); - //the indices of the limbs Force is applied on + //the indices of the limbs Force is applied on //(if none, force is applied only to the limb the attack is attached to) public readonly List ForceOnLimbIndices = new List(); @@ -309,6 +311,10 @@ namespace Barotrauma /// public List Conditionals { get; private set; } = new List(); + /// + /// StatusEffects to apply when the attack triggers. + /// StatusEffect types of 'OnUse' are executed always, 'OnFailure' only when the attack doesn't deal damage and 'OnSuccess' executes when some damage is dealt. + /// private readonly List statusEffects = new List(); public void SetUser(Character user) @@ -322,7 +328,7 @@ namespace Barotrauma // used for talents/ability conditions public Item SourceItem { get; set; } - + public List GetMultipliedAfflictions(float multiplier) { List multipliedAfflictions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 4df2d8591..d7c1d099e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -11,6 +11,7 @@ using FarseerPhysics.Dynamics; using Barotrauma.Extensions; using System.Collections.Immutable; using Barotrauma.Abilities; +using System.Diagnostics; #if SERVER using System.Text; #endif @@ -756,7 +757,7 @@ namespace Barotrauma /// public bool InPressure { - get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; } + get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; } } /// @@ -964,6 +965,7 @@ namespace Barotrauma /// The secondary selected item. It's an item other than a device (see ), e.g. a ladder or a chair. /// public Item SelectedSecondaryItem { get; set; } + public void ReleaseSecondaryItem() => SelectedSecondaryItem = null; /// /// Has the characters selected a primary or a secondary item? /// @@ -1823,20 +1825,8 @@ namespace Barotrauma public void StackSpeedMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeSpeedMultiplier) - { - greatestNegativeSpeedMultiplier = val; - } - } - else - { - if (val > greatestPositiveSpeedMultiplier) - { - greatestPositiveSpeedMultiplier = val; - } - } + greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier); + greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier); } public void ResetSpeedMultiplier() @@ -1859,20 +1849,8 @@ namespace Barotrauma public void StackHealthMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeHealthMultiplier) - { - greatestNegativeHealthMultiplier = val; - } - } - else - { - if (val > greatestPositiveHealthMultiplier) - { - greatestPositiveHealthMultiplier = val; - } - } + greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier); + greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier); } private void CalculateHealthMultiplier() @@ -2456,6 +2434,8 @@ namespace Barotrauma return true; } + private Stopwatch sw; + private Stopwatch StopWatch => sw ??= new Stopwatch(); private float _selectedItemPriority; private Item _foundItem; /// @@ -2469,14 +2449,20 @@ namespace Barotrauma IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null) { + if (HumanAIController.DebugAI) + { + StopWatch.Restart(); + } if (itemIndex == 0) { _foundItem = null; _selectedItemPriority = 0; } - for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++) + int itemsPerFrame = IsOnPlayerTeam ? 100 : 10; + int checkedItemCount = 0; + for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++) { - itemIndex++; + checkedItemCount++; var item = Item.ItemList[itemIndex]; if (!item.IsInteractable(this)) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } @@ -2516,7 +2502,21 @@ namespace Barotrauma } } targetItem = _foundItem; - return itemIndex >= Item.ItemList.Count - 1; + bool completed = itemIndex >= Item.ItemList.Count - 1; + if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1) + { + var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}"; + if (StopWatch.ElapsedMilliseconds > 5) + { + DebugConsole.ThrowError(msg); + } + else + { + // An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue. + DebugConsole.AddWarning(msg); + } + } + return completed; } public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null; @@ -2536,7 +2536,7 @@ namespace Barotrauma } } - return checkVisibility ? CanSeeCharacter(c) : true; + return !checkVisibility || CanSeeCharacter(c); } public bool CanInteractWith(Item item, bool checkLinked = true) @@ -2930,7 +2930,7 @@ namespace Barotrauma else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent() == null && (focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect)) { - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); #if CLIENT CharacterHealth.OpenHealthWindow = null; #endif @@ -3270,7 +3270,7 @@ namespace Barotrauma } if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem)) { - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); } if (!IsDead) { LockHands = false; } @@ -3484,14 +3484,19 @@ namespace Barotrauma despawnTimer += deltaTime * despawnPriority; if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } - if (IsHuman) + Identifier despawnContainerId = + IsHuman ? + "despawncontainer".ToIdentifier() : + Params.DespawnContainer; + if (!despawnContainerId.IsEmpty) { var containerPrefab = - ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ?? + MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ?? + ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ?? (MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab); if (containerPrefab == null) { - DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red); + DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red); } else { @@ -3615,7 +3620,7 @@ namespace Barotrauma { if (character == this) { continue; } if (character.TeamID != TeamID) { continue; } - if (!(character.AIController is HumanAIController)) { continue; } + if (character.AIController is not HumanAIController) { continue; } if (!HumanAIController.IsActive(character)) { continue; } foreach (var currentOrder in character.CurrentOrders) { @@ -4507,9 +4512,12 @@ namespace Barotrauma OnDeath?.Invoke(this, CauseOfDeath); - var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); - CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); - CauseOfDeath.Killer?.RecordKill(this); + if (CauseOfDeath.Type != CauseOfDeathType.Disconnected) + { + var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); + CauseOfDeath.Killer?.RecordKill(this); + } if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { @@ -4724,7 +4732,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - newItem.CreateStatusEvent(); + newItem.CreateStatusEvent(loadingRound: true); } #if SERVER newItem.GetComponent()?.SyncHistory(); @@ -4981,6 +4989,8 @@ namespace Barotrauma #region Talents private readonly List characterTalents = new List(); + public IReadOnlyCollection CharacterTalents => characterTalents; + public void LoadTalents() { List toBeRemoved = null; @@ -5077,7 +5087,7 @@ namespace Barotrauma public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) { - foreach (var characterTalent in characterTalents) + foreach (CharacterTalent characterTalent in CharacterTalents) { characterTalent.CheckTalent(abilityEffectType, abilityObject); } @@ -5373,7 +5383,7 @@ namespace Barotrauma public void StopClimbing() { AnimController.StopClimbing(); - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 58daaa291..c3f8b6f0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -11,6 +12,31 @@ using Barotrauma.Abilities; namespace Barotrauma { + [NetworkSerialize] + internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct + { + [return: MaybeNull] + public JobVariant ToJobVariant() + { + if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; } + return new JobVariant(jobPrefab, Variant); + } + + public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant); + } + + [NetworkSerialize(ArrayMaxSize = byte.MaxValue)] + internal readonly record struct NetCharacterInfo(string NewName, + ImmutableArray Tags, + byte HairIndex, + byte BeardIndex, + byte MoustacheIndex, + byte FaceAttachmentIndex, + Color SkinColor, + Color HairColor, + Color FacialHairColor, + ImmutableArray JobVariants) : INetSerializableStruct; + class CharacterInfoPrefab { public readonly ImmutableArray Heads; @@ -879,10 +905,19 @@ namespace Barotrauma Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (talentIdentifier == Identifier.Empty) { continue; } + if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab)) + { + foreach (TalentMigration migration in prefab.Migrations) + { + migration.TryApply(version, this); + } + } + UnlockedTalents.Add(talentIdentifier); } } } + LoadHeadAttachments(); } @@ -1856,6 +1891,21 @@ namespace Barotrauma } } + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier) + { + float statValue = GetSavedStatValue(statType, statIdentifier); + + if (GameMain.NetworkMember is null) { return statValue; } + + foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + { + int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier); + statValue = Math.Max(statValue, botStatValue); + } + + return statValue; + } + public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) { if (!SavedStatValues.ContainsKey(statType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 073ff8e37..99b34312b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -14,8 +15,8 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } - public float PendingAdditionStrength { get; set; } - public float AdditionStrength { get; set; } + public float PendingGrainEffectStrength { get; set; } + public float GrainEffectStrength { get; set; } private float fluctuationTimer; @@ -46,7 +47,7 @@ namespace Barotrauma float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); if (newValue > _strength) { - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; Duration = Prefab.Duration; } _strength = newValue; @@ -73,8 +74,7 @@ namespace Barotrauma public float DamagePerSecondTimer; public float PreviousVitalityDecrease; - public float StrengthDiminishMultiplier = 1.0f; - public Affliction MultiplierSource; + public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null); public readonly Dictionary PeriodicEffectTimers = new Dictionary(); @@ -100,7 +100,7 @@ namespace Barotrauma prefab?.ReloadSoundsIfNeeded(); #endif Prefab = prefab; - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; _strength = strength; Identifier = prefab.Identifier; @@ -179,7 +179,7 @@ namespace Barotrauma float currVitalityDecrease = MathHelper.Lerp( currentEffect.MinVitalityDecrease, currentEffect.MaxVitalityDecrease, - (strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(strength)); if (currentEffect.MultiplyByMaxVitality) { @@ -200,11 +200,11 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinGrainStrength, currentEffect.MaxGrainStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); - if (Prefab.GrainBurst > 0 && AdditionStrength > amount) + if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount) { - return Math.Min(AdditionStrength, 1.0f); + return Math.Min(GrainEffectStrength, 1.0f); } return amount; @@ -220,7 +220,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenDistort, currentEffect.MaxScreenDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetRadialDistortStrength() @@ -233,7 +233,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinRadialDistort, currentEffect.MaxRadialDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetChromaticAberrationStrength() @@ -246,7 +246,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinChromaticAberration, currentEffect.MaxChromaticAberration, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetAfflictionOverlayMultiplier() @@ -261,7 +261,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinAfflictionOverlayAlphaMultiplier, currentEffect.MaxAfflictionOverlayAlphaMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetFaceTint() @@ -273,7 +273,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinFaceTint, currentEffect.MaxFaceTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetBodyTint() @@ -285,7 +285,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinBodyTint, currentEffect.MaxBodyTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetScreenBlurStrength() @@ -298,7 +298,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenBlur, currentEffect.MaxScreenBlur, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect) @@ -316,7 +316,7 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinSkillMultiplier, currentEffect.MaxSkillMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return amount; } @@ -347,7 +347,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetSpeedMultiplier() @@ -358,21 +358,16 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, currentEffect.MaxSpeedMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetStatValue(StatTypes statType) { if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; } - if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) - { - return MathHelper.Lerp( - value.minValue, - value.maxValue, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); - } - return 0.0f; + if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; } + + return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this)); } public bool HasFlag(AbilityFlags flagType) @@ -415,13 +410,16 @@ namespace Barotrauma fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency; fluctuationTimer %= 1.0f; - if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted + if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened { - float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) - : characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier))); + float stat = characterHealth.Character.GetStatValue( + Prefab.IsBuff + ? StatTypes.BuffDurationMultiplier + : StatTypes.DebuffDurationMultiplier); - _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; + float durationMultiplier = 1f / (1f + stat); + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier; } else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { @@ -441,14 +439,14 @@ namespace Barotrauma { amount /= Prefab.GrainBurst; } - if (PendingAdditionStrength >= 0) + if (PendingGrainEffectStrength >= 0) { - AdditionStrength += amount; - PendingAdditionStrength -= deltaTime; + GrainEffectStrength += amount; + PendingGrainEffectStrength -= deltaTime; } - else if (AdditionStrength > 0) + else if (GrainEffectStrength > 0) { - AdditionStrength -= amount; + GrainEffectStrength -= amount; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index 44643c736..bc8ac1329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that increases the character's Bloodloss affliction with a rate relative to the strength of the bleeding. + /// class AfflictionBleeding : Affliction { public AfflictionBleeding(AfflictionPrefab prefab, float strength) : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index dca86c889..20a19d64f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -7,6 +7,10 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + /// + /// A special affliction type that gradually makes the character turn into another type of character. + /// See for more details. + /// partial class AfflictionHusk : Affliction { public enum InfectionState @@ -63,6 +67,7 @@ namespace Barotrauma private float DormantThreshold => HuskPrefab.DormantThreshold; private float ActiveThreshold => HuskPrefab.ActiveThreshold; private float TransitionThreshold => HuskPrefab.TransitionThreshold; + private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath; public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index be5da631e..d3bd97c3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -56,8 +57,77 @@ namespace Barotrauma public override void Dispose() { } } + /// + /// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection. + /// class AfflictionPrefabHusk : AfflictionPrefab { + // Use any of these to define which limb the appendage is attached to. + // If multiple are defined, the order of preference is: id, name, type. + public readonly int AttachLimbId; + public readonly string AttachLimbName; + public readonly LimbType AttachLimbType; + + /// + /// The minimum strength at which husk infection will be in the dormant stage. + /// It must be less than or equal to ActiveThreshold. + /// + public readonly float DormantThreshold; + + /// + /// The minimum strength at which husk infection will be in the active stage. + /// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold. + /// + public readonly float ActiveThreshold; + + /// + /// The minimum strength at which husk infection will be in its final stage. + /// It must be greater than or equal to ActiveThreshold. + /// + public readonly float TransitionThreshold; + + /// + /// The minimum strength the affliction must have for the affected character + /// to transform into a husk upon death. + /// + public readonly float TransformThresholdOnDeath; + + /// + /// The species of husk to convert the affected character to + /// once husk infection reaches its final stage. + /// + public readonly Identifier HuskedSpeciesName; + + /// + /// If set to true, all buffs are transferred to the converted + /// character after husk transformation is complete. + /// + public readonly bool TransferBuffs; + + /// + /// If set to true, the affected player will see on-screen messages describing husk infection symptoms + /// and affected bots will speak about their current husk infection stage. + /// + public readonly bool SendMessages; + + /// + /// If set to true, affected characters will have their speech impeded once the affliction + /// reaches the dormant stage. + /// + public readonly bool CauseSpeechImpediment; + + /// + /// If set to false, affected characters will no longer require air + /// once the affliction reaches the active stage. + /// + public readonly bool NeedsAir; + + /// + /// If set to true, affected players will retain control of their character + /// after transforming into a husk. + /// + public readonly bool ControlHusk; + public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type) { HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty); @@ -78,7 +148,7 @@ namespace Barotrauma { AttachLimbId = attachElement.GetAttributeInt("id", -1); AttachLimbName = attachElement.GetAttributeString("name", null); - AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None; + AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None); } else { @@ -96,174 +166,282 @@ namespace Barotrauma DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); + + if (DormantThreshold > ActiveThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})"); + } + if (ActiveThreshold > TransitionThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})"); + } + TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); } - - // Use any of these to define which limb the appendage is attached to. - // If multiple are defined, the order of preference is: id, name, type. - public readonly int AttachLimbId; - public readonly string AttachLimbName; - public readonly LimbType AttachLimbType; - - public float ActiveThreshold, DormantThreshold, TransitionThreshold; - public float TransformThresholdOnDeath; - - public readonly Identifier HuskedSpeciesName; - - public readonly bool TransferBuffs; - public readonly bool SendMessages; - public readonly bool CauseSpeechImpediment; - public readonly bool NeedsAir; - public readonly bool ControlHusk; } + /// + /// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character. + /// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality. + /// + /// When defining a new affliction, the type will be determined by the element name. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// class AfflictionPrefab : PrefabWithUintIdentifier { - public class Effect + /// + /// Effects are the primary way to add functionality to afflictions. + /// + /// + /// + /// + /// Enables the specified flag on the character as long as the effect is active. + /// + /// + /// + /// Flag that will be enabled for the character as long as the effect is active. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Which ability flag to enable. + /// + /// + /// + public sealed class Effect { //this effect is applied when the strength is within this range - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")] public float MinStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")] public float MaxStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")] public float MinVitalityDecrease { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")] public float MaxVitalityDecrease { get; private set; } - //how much the strength of the affliction changes per second - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")] public float StrengthChange { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " + + "vitality, with 1 meaning 100%, instead of the same amount for all species.")] public bool MultiplyByMaxVitality { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")] public float MinScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")] public float MaxScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")] public float MinScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")] public float MaxScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")] public float MinRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")] public float MaxRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")] public float MinChromaticAberration { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")] public float MaxChromaticAberration { get; private set; } - [Serialize("255,255,255,255", IsPropertySaveable.No)] + [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")] public Color GrainColor { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")] public float MinGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")] public float MaxGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: + "The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " + + "Effective fluctuation is proportional to the affliction's current strength.")] public float ScreenEffectFluctuationFrequency { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's lowest strength. " + + "See the list of elements for more details.")] public float MinAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's highest strength. " + + "See the list of elements for more details.")] public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's lowest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MinBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's highest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MaxBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")] public float MinSpeedMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")] public float MaxSpeedMultiplier { get; private set; } - - [Serialize(1.0f, IsPropertySaveable.No)] + + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")] public float MinSkillMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")] public float MaxSkillMultiplier { get; private set; } - private readonly Identifier[] resistanceFor; - public IReadOnlyList ResistanceFor => resistanceFor; + /// + /// A list of identifiers of afflictions that the affected character will be + /// resistant to when this effect is active. + /// + public readonly ImmutableArray ResistanceFor; - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, + description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")] public float MinResistance { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, + description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")] public float MaxResistance { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")] public Identifier Tag { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MinFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MaxFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")] public Color MinBodyTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")] public Color MaxBodyTint { get; private set; } /// - /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// - public Identifier[] BlockTransformation { get; private set; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public readonly struct AppliedStatValue + { + /// + /// Which StatType to apply + /// + public readonly StatTypes StatType; - public readonly Dictionary AfflictionStatValues = new Dictionary(); - public AbilityFlags AfflictionAbilityFlags; + /// + /// Minimum value to apply + /// + public readonly float MinValue; + + /// + /// Minimum value to apply + /// + public readonly float MaxValue; + + /// + /// Constant value to apply, will be ignored if MinValue or MaxValue are set + /// + private readonly float Value; + + public AppliedStatValue(ContentXElement element) + { + Value = element.GetAttributeFloat("value", 0.0f); + StatType = element.GetAttributeEnum("stattype", StatTypes.None); + MinValue = element.GetAttributeFloat("minvalue", Value); + MaxValue = element.GetAttributeFloat("maxvalue", Value); + } + } + + /// + /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character. + /// + public readonly ImmutableArray BlockTransformation; + + /// + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. + /// + public readonly ImmutableDictionary AfflictionStatValues; + + public readonly AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active - public readonly List StatusEffects = new List(); + public readonly ImmutableArray StatusEffects; public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); - resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty()); - BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty()); + ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty())!.ToImmutableArray(); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty())!.ToImmutableArray(); + var afflictionStatValues = new Dictionary(); + var statusEffects = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; case "statvalue": - var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName); - - float defaultValue = subElement.GetAttributeFloat("value", 0f); - float minValue = subElement.GetAttributeFloat("minvalue", defaultValue); - float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue); - - AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); + var newStatValue = new AppliedStatValue(subElement); + afflictionStatValues.Add(newStatValue.StatType, newStatValue); break; case "abilityflag": - var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); + AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None); + if (flagType is AbilityFlags.None) + { + DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\"."); + continue; + } AfflictionAbilityFlags |= flagType; break; case "affliction": @@ -271,21 +449,77 @@ namespace Barotrauma break; } } + AfflictionStatValues = afflictionStatValues.ToImmutableDictionary(); + StatusEffects = statusEffects.ToImmutableArray(); } + + /// + /// Returns 0 if affliction.Strength is MinStrength, + /// 1 if affliction.Strength is MaxStrength + /// + public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength); + + /// + /// Returns 0 if affliction.Strength is MinStrength, + /// 1 if affliction.Strength is MaxStrength + /// + public float GetStrengthFactor(float strength) + => MathUtils.InverseLerp( + MinStrength, + MaxStrength, + strength); } - public class Description + /// + /// The description element can be used to define descriptions for the affliction which are shown under specific conditions; + /// for example a description that only shows to other players or only at certain strength levels. + /// + /// + /// + /// Raw text for the description. + /// + /// + public sealed class Description { public enum TargetType { + /// + /// Everyone can see the description. + /// Any, + /// + /// Only the affected character can see the description. + /// Self, + /// + /// The affected character cannot see the description but others can. + /// OtherCharacter } + /// + /// Raw text for the description. + /// public readonly LocalizedString Text; + + /// + /// Text tag used to set the text from the localization files. + /// public readonly Identifier TextTag; - public readonly float MinStrength, MaxStrength; + + /// + /// Minimum strength required for the description to be shown. + /// + public readonly float MinStrength; + + /// + /// Maximum strength required for the description to be shown. + /// + public readonly float MaxStrength; + + /// + /// Who can see the description. + /// public readonly TargetType Target; public Description(ContentXElement element, AfflictionPrefab affliction) @@ -315,7 +549,23 @@ namespace Barotrauma } } - public class PeriodicEffect + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// + /// + /// + /// + /// How often the status effect is applied in seconds. + /// Setting this attribute will set both the min and max interval to the specified value. + /// + /// + /// Minimum interval between applying the status effect in seconds. + /// + /// + /// Maximum interval between applying the status effect in seconds. + /// + /// + public sealed class PeriodicEffect { public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; @@ -368,52 +618,124 @@ namespace Barotrauma public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public override void Dispose() { } - public static IEnumerable List => Prefabs; - // Arbitrary string that is used to identify the type of the affliction. - public readonly Identifier AfflictionType; + public override void Dispose() { } private readonly ContentXElement configElement; - - //Does the affliction affect a specific limb or the whole character - public readonly bool LimbSpecific; - - //If not a limb-specific affliction, which limb is the indicator shown on in the health menu - //(e.g. mental health problems on head, lack of oxygen on torso...) - public readonly LimbType IndicatorLimb; public readonly LocalizedString Name; - public readonly Identifier TranslationIdentifier; - public readonly bool IsBuff; - public readonly bool AffectMachines; - public readonly bool HealableInMedicalClinic; - public readonly float HealCostMultiplier; - public readonly int BaseHealCost; - public readonly bool ShowBarInHealthMenu; - + public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; private readonly LocalizedString defaultDescription; public readonly ImmutableList Descriptions; + /// + /// Arbitrary string that is used to identify the type of the affliction. + /// + public readonly Identifier AfflictionType; + + /// + /// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character. + /// + public readonly bool LimbSpecific; + + /// + /// If the affliction doesn't affect individual limbs, this attribute determines + /// where the game will render the affliction's indicator when viewed in the + /// in-game health UI. + /// + /// For example, the psychosis indicator is rendered on the head, and low oxygen + /// is rendered on the torso. + /// + public readonly LimbType IndicatorLimb; + + /// + /// Can be set to the identifier of another affliction to make this affliction + /// reuse the same name and description. + /// + public readonly Identifier TranslationIdentifier; + + /// + /// If set to true, the game will recognize this affliction as a buff. + /// This means, among other things, that bots won't attempt to treat it, + /// and the health UI will render the affected limb in green rather than red. + /// + public readonly bool IsBuff; + + /// + /// If set to true, this affliction can affect characters that are marked as + /// machines, such as the Fractal Guardian. + /// + public readonly bool AffectMachines; + + /// + /// If set to true, this affliction can be healed at the medical clinic. + /// + /// + /// + /// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise. + /// + /// + public readonly bool HealableInMedicalClinic; + + /// + /// How much each unit of this affliction's strength will add + /// to the cost of healing at the medical clinic. + /// + public readonly float HealCostMultiplier; + + /// + /// The minimum cost of healing this affliction at the medical clinic. + /// + public readonly int BaseHealCost; + + /// + /// If set to false, the health UI will not show the strength of the affliction + /// as a bar under its indicator. + /// + public readonly bool ShowBarInHealthMenu; + + /// + /// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds. + /// public readonly bool HideIconAfterDelay; - //how high the strength has to be for the affliction to take affect + /// + /// How high the strength has to be for the affliction to take effect + /// public readonly float ActivationThreshold = 0.0f; - //how high the strength has to be for the affliction icon to be shown in the UI + + /// + /// How high the strength has to be for the affliction icon to be shown in the UI + /// public readonly float ShowIconThreshold = 0.05f; - //how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + + /// + /// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + /// public readonly float ShowIconToOthersThreshold = 0.05f; + + /// + /// The maximum strength this affliction can have. + /// public readonly float MaxStrength = 100.0f; + /// + /// The strength of the radiation grain effect to apply when the strength of this affliction increases. + /// public readonly float GrainBurst; - //how high the strength has to be for the affliction icon to be shown with a health scanner + /// + /// How high the strength has to be for the affliction icon to be shown with a health scanner + /// public readonly float ShowInHealthScannerThreshold = 0.05f; - //how strong the affliction needs to be before bots attempt to treat it + /// + /// How strong the affliction needs to be before bots attempt to treat it. + /// Also effects when the affliction is shown in the suitable treatments list. + /// public readonly float TreatmentThreshold = 5.0f; /// @@ -422,43 +744,84 @@ namespace Barotrauma public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; /// - /// The affliction is automatically removed after this time. 0 = unlimited + /// The duration of the affliction, in seconds. If set to 0, the affliction does not expire. /// public readonly float Duration; - //how much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// + /// How much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// public float KarmaChangeOnApplied; + /// + /// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength. + /// public readonly float BurnOverlayAlpha; + + /// + /// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength. + /// public readonly float DamageOverlayAlpha; - //steam achievement given when the controlled character receives the affliction + /// Steam achievement given when the controlled character receives the affliction. + /// public readonly Identifier AchievementOnReceived; - //steam achievement given when the affliction is removed from the controlled character + /// + /// Steam achievement given when the affliction is removed from the controlled character. + /// public readonly Identifier AchievementOnRemoved; - public readonly Sprite Icon; + /// + /// A gradient that defines which color to render this affliction's icon + /// with, based on the affliction's current strength. + /// public readonly Color[] IconColors; - public readonly Sprite AfflictionOverlay; + /// + /// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength. + /// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects. + /// public readonly bool AfflictionOverlayAlphaIsLinear; + /// + /// If set to true, this affliction will not persist between rounds. + /// + public readonly bool ResetBetweenRounds; + + /// + /// Should damage particles be emitted when a character receives this affliction? + /// Only relevant if the affliction is of the type "bleeding" or "damage". + /// public readonly bool DamageParticles; /// /// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target. - /// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile. - /// If the affliction is of type buff, the skill is increased only when the target is friendly. + /// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile. + /// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly. /// public readonly float MedicalSkillGain; + /// /// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target. /// The skill is increased only when the target is hostile. /// public readonly float WeaponsSkillGain; + /// + /// A list of species this affliction is allowed to affect. + /// + public Identifier[] TargetSpecies { get; protected set; } + + /// + /// Effects to apply at various strength levels. + /// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap. + /// private readonly List effects = new List(); + + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// private readonly List periodicEffects = new List(); public IEnumerable Effects => effects; @@ -467,9 +830,16 @@ namespace Barotrauma private readonly ConstructorInfo constructor; - public Identifier[] TargetSpecies { get; protected set; } + /// + /// An icon that’s used in the UI to represent this affliction. + /// + public readonly Sprite Icon; - public readonly bool ResetBetweenRounds; + /// + /// A sprite that covers the affected player's entire screen when this affliction is active. + /// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier + /// + public readonly Sprite AfflictionOverlay; public IEnumerable> TreatmentSuitability { @@ -497,7 +867,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(fallbackName)) { Name = Name.Fallback(fallbackName); - } + } defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index 724184a11..a653041ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that makes the character see and hear things that aren't there. + /// partial class AfflictionPsychosis : Affliction { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 408545fa2..fe656968e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -1,11 +1,12 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; namespace Barotrauma { + /// + /// A special affliction type that periodically inverts the character's controls and stuns the character. + /// The frequency and duration of the effects increases the higher the strength of the affliction is. + /// class AfflictionSpaceHerpes : Affliction { private float invertControlsCooldown = 60.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 730a08b4d..e3aaf11d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -3,6 +3,10 @@ using System; namespace Barotrauma { + /// + /// A special affliction type that increases the duration of buffs (afflictions of the type "buff"). The increase is defined using the + /// and attributes of the affliction effect. + /// class BuffDurationIncrease : Affliction { public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength) @@ -20,9 +24,9 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; } - affliction.MultiplierSource = null; - affliction.StrengthDiminishMultiplier = 1f; + if (!affliction.Prefab.IsBuff || affliction == this || affliction.StrengthDiminishMultiplier.Source != this) { continue; } + affliction.StrengthDiminishMultiplier.Source = null; + affliction.StrengthDiminishMultiplier.Value = 1f; } } else @@ -31,10 +35,10 @@ namespace Barotrauma { if (!affliction.Prefab.IsBuff || affliction == this) { continue; } float multiplier = GetDiminishMultiplier(); - if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; } + if (affliction.StrengthDiminishMultiplier.Value < multiplier && affliction.StrengthDiminishMultiplier.Source != this) { continue; } - affliction.MultiplierSource = this; - affliction.StrengthDiminishMultiplier = multiplier; + affliction.StrengthDiminishMultiplier.Source = this; + affliction.StrengthDiminishMultiplier.Value = multiplier; } } } @@ -48,7 +52,7 @@ namespace Barotrauma float multiplier = MathHelper.Lerp( currentEffect.MinBuffMultiplier, currentEffect.MaxBuffMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return 1.0f / Math.Max(multiplier, 0.001f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 12f7dbc87..250337640 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -184,7 +184,7 @@ namespace Barotrauma } } - public Color DefaultFaceTint = Color.TransparentBlack; + public static readonly Color DefaultFaceTint = Color.TransparentBlack; public Color FaceTint { @@ -449,7 +449,11 @@ namespace Barotrauma var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + + resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + if (resistance > 1f) { resistance = 1f; } + + return resistance; } public float GetStatValue(StatTypes statType) @@ -1151,16 +1155,14 @@ namespace Barotrauma } } - public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); - private readonly HashSet afflictionTags = new HashSet(); - public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + public IEnumerable GetActiveAfflictionTags() { afflictionTags.Clear(); - foreach (Affliction affliction in afflictions) + foreach (Affliction affliction in afflictions.Keys) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !currentEffect.Tag.IsEmpty) + if (currentEffect is { Tag.IsEmpty: false }) { afflictionTags.Add(currentEffect.Tag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 18ff8ac7f..e7d13cf51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -111,6 +111,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.No)] public Identifier Group { get; set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AllowDraggingIndefinitely { get; set; } + public XElement Element { get; protected set; } @@ -214,7 +217,8 @@ namespace Barotrauma CharacterInfo characterInfo; if (characterElement == null) { - characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync);} + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync); + } else { characterInfo = new CharacterInfo(characterElement, Identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 2db2aaadf..e311800bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -50,7 +50,7 @@ namespace Barotrauma public override void Dispose() { } } - class JobVariant + internal class JobVariant { public JobPrefab Prefab; public int Variant; @@ -113,7 +113,7 @@ namespace Barotrauma public readonly LocalizedString Name; - [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No, description: "How should the character behave when idling (not doing any particular task)?")] public AIObjectiveIdle.BehaviorType IdleBehavior { get; @@ -122,78 +122,63 @@ namespace Barotrauma public readonly LocalizedString Description; - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can the character speak any random lines, or just ones specifically meant for the job?")] public bool OnlyJobSpecificDialog { get; private set; } - //the number of these characters in the crew the player starts with in the single player campaign - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "The number of these characters in the crew the player starts with in the single player campaign.")] public int InitialCount { get; private set; } - //if set to true, a client that has chosen this as their preferred job will get it no matter what - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")] public bool AllowAlways { get; private set; } - //how many crew members can have the job (only one captain etc) - [Serialize(100, IsPropertySaveable.No)] + [Serialize(100, IsPropertySaveable.No, description: "How many crew members can have the job (e.g. only one captain etc).")] public int MaxNumber { get; private set; } - //how many crew members are REQUIRED to have the job - //(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference) - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "How many crew members are required to have the job. I.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference.")] public int MinNumber { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum amount of karma a player must have to get assigned this job.")] public float MinKarma { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier on the base hiring cost when hiring the character from an outpost.")] public float PriceMultiplier { get; private set; } - // TODO: not used - [Serialize(10.0f, IsPropertySaveable.No)] - public float Commonness - { - get; - private set; - } - - //how much the vitality of the character is increased/reduced from the default value - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the vitality of the character is increased/reduced from the default value (e.g. 10 = 110 total vitality if the default vitality is 100.).")] public float VitalityModifier { get; private set; } - //whether the job should be available to NPCs - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Hidden jobs are not selectable by players, but can be used by e.g. outpost NPCs.")] public bool HiddenJob { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index ba88edee5..1c1d10a9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -119,6 +119,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] public float AISlowTurretPriority { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable] + public Identifier DespawnContainer { get; private set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs index 4fe2c1c86..c78b2c66b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -6,11 +6,13 @@ namespace Barotrauma.Abilities { private readonly ItemTalentStats stat; private readonly float value; + private readonly bool stackable; public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); + stackable = abilityElement.GetAttributeBool("stackable", true); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) @@ -25,7 +27,7 @@ namespace Barotrauma.Abilities { if (abilityObject is not IAbilityItem ability) { return; } - ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent); + ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 6c4022968..4185832c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -9,12 +9,14 @@ namespace Barotrauma.Abilities private readonly ItemTalentStats stat; private readonly float value; private readonly ImmutableHashSet tags; + private readonly bool stackable; public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + stackable = abilityElement.GetAttributeBool("stackable", true); } public override void InitializeAbility(bool addingFirstTime) @@ -41,7 +43,7 @@ namespace Barotrauma.Abilities { if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { - item.StatManager.ApplyStat(stat, value, CharacterTalent); + item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index bb6a2ac0a..6b59a5825 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -217,6 +217,11 @@ namespace Barotrauma.Abilities public static StatTypes ParseStatType(string statTypeString, string debugIdentifier) { + //backwards compatibility + if (statTypeString.Equals("MedicalItemDurationMultiplier", StringComparison.OrdinalIgnoreCase)) + { + statTypeString = "BuffItemApplyingMultiplier"; + } if (!Enum.TryParse(statTypeString, true, out StatTypes statType)) { DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 4cc374e79..0cb53857e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -71,6 +71,29 @@ namespace Barotrauma } } + private static readonly HashSet checkedNonStackableTalents = new(); + + /// + /// Checks talents for a given AbilityObject taking into account non-stackable talents. + /// + public static void CheckTalentsForCrew(IEnumerable crew, AbilityEffectType type, AbilityObject abilityObject) + { + checkedNonStackableTalents.Clear(); + foreach (Character character in crew) + { + foreach (CharacterTalent characterTalent in character.CharacterTalents) + { + if (!characterTalent.Prefab.AbilityEffectsStackWithSameTalent) + { + if (checkedNonStackableTalents.Contains(characterTalent.Prefab.Identifier)) { continue; } + checkedNonStackableTalents.Add(characterTalent.Prefab.Identifier); + } + + characterTalent.CheckTalent(type, abilityObject); + } + } + } + public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject) { if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs new file mode 100644 index 000000000..b185d09cd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs @@ -0,0 +1,109 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + internal abstract class TalentMigration + { + private readonly Version version; + + private delegate TalentMigration TalentMigrationCtor(Version version, ContentXElement element); + + private static readonly Dictionary migrationTemplates = + new() + { + [new Identifier("AddStat")] = + static (version, element) => new TalentMigrationAddStat(version, element), + + [new Identifier("UpdateStatIdentifier")] = + static (version, element) => new TalentMigrationUpdateStatIdentifier(version, element) + }; + + public bool TryApply(Version savedVersion, CharacterInfo info) + { + if (version <= savedVersion) { return false; } + Apply(info); + return true; + } + + protected abstract void Apply(CharacterInfo info); + + protected TalentMigration(Version targetVersion) + { + version = targetVersion; + } + + public static TalentMigration FromXML(ContentXElement element) + { + Version? version = XMLExtensions.GetAttributeVersion(element, "version", null); + + if (version is null) + { + throw new Exception("Talent migration version not defined."); + } + + Identifier name = element.Name.ToString().ToIdentifier(); + + if (!migrationTemplates.TryGetValue(name, out TalentMigrationCtor? ctor)) + { + throw new Exception($"Unknown talent migration type: {name}."); + } + + return ctor(version, element); + } + } + + /// + /// Migration that adds a missing permanent stat to the character. + /// + internal sealed class TalentMigrationAddStat : TalentMigration + { + [Serialize(StatTypes.None, IsPropertySaveable.Yes)] + public StatTypes StatType { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier StatIdentifier { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes)] + public float Value { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool RemoveOnDeath { get; set; } + + public TalentMigrationAddStat(Version targetVersion, ContentXElement element) : base(targetVersion) + => SerializableProperty.DeserializeProperties(this, element); + + protected override void Apply(CharacterInfo info) + { + info.ChangeSavedStatValue(StatType, Value, StatIdentifier, RemoveOnDeath); + } + } + + /// + /// Migration that updates permanent stat identifiers. + /// + internal class TalentMigrationUpdateStatIdentifier : TalentMigration + { + [Serialize("", IsPropertySaveable.Yes, "The old identifier to update.")] + public Identifier Old { get; set; } + + [Serialize("", IsPropertySaveable.Yes, "What to change the old identifier to.")] + public Identifier New { get; set; } + + public TalentMigrationUpdateStatIdentifier(Version targetVersion, ContentXElement element) : base(targetVersion) + => SerializableProperty.DeserializeProperties(this, element); + + protected override void Apply(CharacterInfo info) + { + foreach (SavedStatValue statValue in info.SavedStatValues.Values.SelectMany(static s => s)) + { + if (statValue.StatIdentifier != Old) { continue; } + + statValue.StatIdentifier = New; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index d3cc70e56..bd105e729 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,4 +1,7 @@ -#if CLIENT +using System; +using System.Collections.Immutable; +using System.Xml.Linq; +#if CLIENT using Microsoft.Xna.Framework; #endif @@ -12,6 +15,11 @@ namespace Barotrauma public LocalizedString Description { get; private set; } + /// + /// When set to false the AbilityEffects of multiple of the same talent will not be checked and only the first one. + /// + public bool AbilityEffectsStackWithSameTalent; + public readonly Sprite Icon; #if CLIENT @@ -20,6 +28,8 @@ namespace Barotrauma public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); + public readonly ImmutableHashSet Migrations; + public ContentXElement ConfigElement { get; @@ -32,6 +42,8 @@ namespace Barotrauma DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); + AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true); + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); if (!nameIdentifier.IsEmpty) { @@ -48,6 +60,8 @@ namespace Barotrauma : Option.None(); #endif + var migrations = ImmutableHashSet.CreateBuilder(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -60,9 +74,25 @@ namespace Barotrauma TextManager.ConstructDescription(ref tempDescription, subElement); Description = tempDescription; break; + case "migrations": + foreach (var migrationElement in subElement.Elements()) + { + try + { + var migration = TalentMigration.FromXML(migrationElement); + migrations.Add(migration); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e); + } + } + break; } } + Migrations = migrations.ToImmutable(); + if (element.GetAttribute("description") != null) { string description = element.GetAttributeString("description", string.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index b4b26579b..d4a75eb27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using System.Xml.Linq; @@ -22,7 +23,7 @@ namespace Barotrauma : string.Empty); } - public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0); + public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 11, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -203,7 +204,13 @@ namespace Barotrauma break; } } - + + if (UgcId.TryUnwrap(out ContentPackageId? id) && id != null) + { + incrementalHash.AppendData(Encoding.UTF8.GetBytes(id.StringRepresentation)); + } + incrementalHash.AppendData(Encoding.UTF8.GetBytes(ModVersion)); + var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset()); if (logging) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 88ff41f52..3f3249990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -116,9 +116,12 @@ namespace Barotrauma public static void ThrowIfDuplicates(IEnumerable pkgs) { var contentPackages = pkgs as IList ?? pkgs.ToArray(); - if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2))) + foreach (ContentPackage cp in contentPackages) { - throw new InvalidOperationException($"Input contains duplicate packages"); + if (contentPackages.AtLeast(2, cp2 => cp == cp2)) + { + throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index dbb30b209..84dde5cb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -384,7 +384,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - PermissionPreset.List.Select(pp => pp.Name.Value).ToArray() + PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray() }; })); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 0b8c80af8..01763954d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -75,88 +75,390 @@ namespace Barotrauma OnStatusEffectIdentifier, } + /// + /// StatTypes are used to alter several traits of a character. They are mostly used by talents. + /// + /// A lot of StatTypes use a "percentage" value. The way this works is that the value is 0 by default and 1 is added to the value of the stat type to get the final multiplier. + /// For example if the value is set to 0.2 then 1 is added to it making it 1.2 and that is used as a multiplier. + /// This makes it so values between -100% and +100% can be easily represented as -1 and 1 respectively. For example 0.5 would translate to 1.5 for +50% and -0.2 would translate to 0.8 for -20% multiplier. + /// public enum StatTypes { + /// + /// Used to indicate an invalid stat type. Should not be used. + /// None, - // Skills + + /// + /// Boosts electrical skill by a flat amount. + /// ElectricalSkillBonus, + + /// + /// Boosts helm skill by a flat amount. + /// HelmSkillBonus, - HelmSkillOverride, - MedicalSkillOverride, - WeaponsSkillOverride, - ElectricalSkillOverride, - MechanicalSkillOverride, + + /// + /// Boosts mechanical skill by a flat amount. + /// MechanicalSkillBonus, + + /// + /// Boosts medical skill by a flat amount. + /// MedicalSkillBonus, + + /// + /// Boosts weapons skill by a flat amount. + /// WeaponsSkillBonus, - // Character attributes + + /// + /// Boosts the character's helm skill to the given value if it's lower than the given value. + /// + HelmSkillOverride, + + /// + /// Boosts the character's medical skill to the given value if it's lower than the given value. + /// + MedicalSkillOverride, + + /// + /// Boosts the character's weapons skill to the given value if it's lower than the given value. + /// + WeaponsSkillOverride, + + /// + /// Boosts the character's electrical skill to the given value if it's lower than the given value. + /// + ElectricalSkillOverride, + + /// + /// Boosts the character's mechanical skill to the given value if it's lower than the given value. + /// + MechanicalSkillOverride, + + /// + /// Increases character's maximum vitality by a percentage. + /// MaximumHealthMultiplier, + + /// + /// Increases both walking and swimming speed of the character by a percentage. + /// MovementSpeed, + + /// + /// Increases the character's walking speed by a percentage. + /// WalkingSpeed, + + /// + /// Increases the character's swimming speed by a percentage. + /// SwimmingSpeed, + + /// + /// Decreases how long it takes for buffs applied to the character decay over time by a percentage. + /// Buffs are afflictions that have isBuff set to true. + /// BuffDurationMultiplier, + + /// + /// Decreases how long it takes for debuff applied to the character decay over time by a percentage. + /// Debuffs are afflictions that have isBuff set to false. + /// DebuffDurationMultiplier, + + /// + /// Increases the strength of afflictions that are applied to the character by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemEffectivenessMultiplier, + + /// + /// Increases the resistance to pushing force caused by flowing water by a percentage. The resistance cannot be below 0% or higher than 100%. + /// FlowResistance, - // Combat + + /// + /// Increases how much damage the character deals via all attacks by a percentage. + /// AttackMultiplier, + + /// + /// Increases how much damage the character deals to other characters on the same team by a percentage. + /// TeamAttackMultiplier, + + /// + /// Decreases the reload time of ranged weapons held by the character by a percentage. + /// RangedAttackSpeed, + + /// + /// Decreases the reload time of submarine turrets operated by the character by a percentage. + /// TurretAttackSpeed, + + /// + /// Decreases the power consumption of submarine turrets operated by the character by a percentage. + /// TurretPowerCostReduction, + + /// + /// Increases how fast submarine turrets operated by the character charge up by a percentage. Affects turrets like pulse laser. + /// TurretChargeSpeed, + + /// + /// Increases how fast the character can swing melee weapons by a percentage. + /// MeleeAttackSpeed, + + /// + /// Increases the damage dealt by melee weapons held by the character by a percentage. + /// MeleeAttackMultiplier, - RangedAttackMultiplier, + + /// + /// Decreases the spread of ranged weapons held by the character by a percentage. + /// RangedSpreadReduction, - // Utility + + /// + /// Increases the repair speed of the character by a percentage. + /// RepairSpeed, + + /// + /// Increases the repair speed of the character when repairing mechanical items by a percentage. + /// MechanicalRepairSpeed, + + /// + /// Increase deconstruction speed of deconstructor operated by the character by a percentage. + /// DeconstructorSpeedMultiplier, + + /// + /// Increases the repair speed of repair tools that fix submarine walls by a percentage. + /// RepairToolStructureRepairMultiplier, + + /// + /// Increases the wall damage of tools that destroy submarine walls like plasma cutter by a percentage. + /// RepairToolStructureDamageMultiplier, + + /// + /// Increase the detach speed of items like minerals that require a tool to detach from the wall by a percentage. + /// RepairToolDeattachTimeMultiplier, + + /// + /// Allows the character to repair mechanical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair mechanical items to 110% condition. + /// MaxRepairConditionMultiplierMechanical, + + /// + /// Allows the character to repair electrical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair electrical items to 110% condition. + /// MaxRepairConditionMultiplierElectrical, + + /// + /// Increase the the quality of items crafted by the character by a flat amount. + /// Can be made to only affect certain item with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all items. + /// IncreaseFabricationQuality, + + /// + /// Boosts the condition of genes combined by the character by a flat amount. + /// GeneticMaterialRefineBonus, + + /// + /// Reduces the chance to taint a gene when combining genes by a percentage. Tainting probability can not go below 0% or above 100%. + /// GeneticMaterialTaintedProbabilityReductionOnCombine, + + /// + /// Increases the speed at which the character gains skills by a percentage. + /// SkillGainSpeed, + + /// + /// Whenever the character's skill level up add a flat amount of more skill levels to the character. + /// ExtraLevelGain, + + /// + /// Increases the speed at which the character gains helm skill by a percentage. + /// HelmSkillGainSpeed, + + /// + /// Increases the speed at which the character gains weapons skill by a percentage. + /// WeaponsSkillGainSpeed, + + /// + /// Increases the speed at which the character gains medical skill by a percentage. + /// MedicalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains electrical skill by a percentage. + /// ElectricalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains mechanical skill by a percentage. + /// MechanicalSkillGainSpeed, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemApplyingMultiplier, - MedicalItemDurationMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have isBuff set to true. + /// + BuffItemApplyingMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have "poison" type. + /// PoisonMultiplier, - // Tinker + + /// + /// Increases how long the character can tinker with items by a flat amount where 1 = 1 second. + /// TinkeringDuration, + + /// + /// Increases the effectiveness of the character's tinkerings by a percentage. + /// Tinkering strength affects the speed and effectiveness of the item that is being tinkered with. + /// TinkeringStrength, + + /// + /// Increases how much condition tinkered items lose when the character tinkers with them by a percentage. + /// TinkeringDamage, - // Misc + + /// + /// Increases how much reputation the character gains by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationGainMultiplier, + + /// + /// Increases how much reputation the character loses by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationLossMultiplier, + + /// + /// Increases how much money the character gains from missions by a percentage. + /// MissionMoneyGainMultiplier, + + /// + /// Increases how much talent experience the character gains from all sources by a percentage. + /// ExperienceGainMultiplier, + + /// + /// Increases how much talent experience the character gains from missions by a percentage. + /// MissionExperienceGainMultiplier, + + /// + /// Increases how many missions the characters crew can have at the same time by a flat amount. + /// ExtraMissionCount, + + /// + /// Increases how many items are in stock in special sales in the store by a flat amount. + /// ExtraSpecialSalesCount, + + /// + /// Increases how much money is gained from selling items to the store by a percentage. + /// StoreSellMultiplier, + + /// + /// Decreases the prices of items in affiliated store by a percentage. + /// StoreBuyMultiplierAffiliated, + + /// + /// Decreases the prices of items in all stores by a percentage. + /// StoreBuyMultiplier, + + /// + /// Decreases the price of upgrades and submarines in affiliated outposts by a percentage. + /// ShipyardBuyMultiplierAffiliated, + + /// + /// Decreases the price of upgrades and submarines in all outposts by a percentage. + /// ShipyardBuyMultiplier, + + /// + /// Limits how many of a certain item can be attached to the wall in the submarine at the same time. + /// Has to be used with CharacterAbilityGivePermanentStat to specify the tag of the item that is affected. Does nothing if no tag is specified. + /// MaxAttachableCount, + + /// + /// Increase the radius of explosions caused by the character by a percentage. + /// ExplosionRadiusMultiplier, + + /// + /// Increases the damage of explosions caused by the character by a percentage. + /// ExplosionDamageMultiplier, + + /// + /// Decreases the time it takes to fabricate items on fabricators operated by the character by a percentage. + /// FabricationSpeed, + + /// + /// Increases how much damage the character deals to ballast flora by a percentage. + /// BallastFloraDamageMultiplier, + + /// + /// Increases the time it takes for the character to pass out when out of oxygen. + /// HoldBreathMultiplier, + + /// + /// Used to set the character's apprencticeship to a certain job. + /// Used by the "apprenticeship" talent and requires a job to be specified via CharacterAbilityGivePermanentStat. + /// Apprenticeship, - Affiliation, + + /// + /// Increases the revival chance of the character when performing CPR by a percentage. + /// CPRBoost, + + /// + /// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat. + /// LockedTalents } @@ -175,22 +477,77 @@ namespace Barotrauma FabricationSpeed } + /// + /// AbilityFlags are a set of toggleable flags that can be applied to characters. + /// [Flags] public enum AbilityFlags { + /// + /// Used to indicate an erroneous ability flag. Should not be used. + /// None = 0, + + /// + /// Character will not be able to run. + /// MustWalk = 0x1, + + /// + /// Character is immune to pressure. + /// ImmuneToPressure = 0x2, + + /// + /// Character won't be targeted by enemy AI. + /// IgnoredByEnemyAI = 0x4, + + /// + /// Character can drag corpses without a movement speed penalty. + /// MoveNormallyWhileDragging = 0x8, + + /// + /// Character is able to tinker with items. + /// CanTinker = 0x10, + + /// + /// Character is able to tinker with fabricators and deconstructors. + /// CanTinkerFabricatorsAndDeconstructors = 0x20, + + /// + /// Allows items tinkered by the character to consume no power. + /// TinkeringPowersDevices = 0x40, + + /// + /// Allows the character to gain skills past 100. + /// GainSkillPastMaximum = 0x80, + + /// + /// Allows the character to retain experience when respawning as a new character. + /// RetainExperienceForNewCharacter = 0x100, + + /// + /// Allows CharacterAbilityApplyStatusEffectsToLastOrderedCharacter to affect the last 2 characters ordered. + /// AllowSecondOrderedTarget = 0x200, + + /// + /// Character will stay conscious even if their vitality drops below 0. + /// AlwaysStayConscious = 0x400, - CanNotDieToAfflictions = 0x800, + + /// + /// Prevents afflictions on the character from dropping the characters vitality below the kill threshold. + /// The character can still die from sources like getting crushed by pressure or if their head is severed. + /// + CanNotDieToAfflictions = 0x800 } [Flags] @@ -228,4 +585,4 @@ namespace Barotrauma Local, Radio } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 192664d17..1b41e3bf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -9,6 +9,8 @@ namespace Barotrauma public event Action Finished; protected bool isFinished; + public int RandomSeed; + protected readonly EventPrefab prefab; public EventPrefab Prefab => prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 957508205..be2f85fbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -31,6 +31,8 @@ namespace Barotrauma private bool isFinished; + private readonly Random random; + public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) @@ -42,6 +44,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); + random = new MTRandom(parentEvent.RandomSeed); } public override bool IsFinished(ref string goTo) @@ -80,7 +83,7 @@ namespace Barotrauma } else if (!MissionTag.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random); } if (campaign is MultiPlayerCampaign mpCampaign) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 5d86e91d8..f05962579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -31,7 +31,8 @@ namespace Barotrauma if (Wait) { - var gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) + var gotoObjective = new AIObjectiveGoTo( + AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = 100.0f, SourceEventAction = this diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 84d8eaa1b..f015fc26b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -188,6 +188,10 @@ namespace Barotrauma outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); } } +#if SERVER + newCharacter.LoadTalents(); + GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); +#endif }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index a6f01c406..ae7e67384 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -72,7 +73,7 @@ namespace Barotrauma private readonly List activeEvents = new List(); - private readonly HashSet finishedEvents = new HashSet(); + private readonly HashSet finishedEvents = new HashSet(); private readonly HashSet nonRepeatableEvents = new HashSet(); private readonly HashSet usedUniqueSets = new HashSet(); @@ -123,7 +124,8 @@ namespace Barotrauma public bool Enabled = true; - private MTRandom rand; + private MTRandom random; + private int randomSeed; public void StartRound(Level level) { @@ -147,23 +149,22 @@ namespace Barotrauma } SelectSettings(); - - int seed = 0; + if (level != null) { - seed = ToolBox.StringToInt(level.Seed); + randomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.IdentifierToInt(previousEvent); + randomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } - rand = new MTRandom(seed); + random = new MTRandom(randomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { @@ -171,7 +172,7 @@ namespace Barotrauma initialEventSet = SelectRandomEvents( EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); } if (initialEventSet != null) { @@ -366,20 +367,49 @@ namespace Barotrauma /// /// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history /// - public void RegisterEventHistory() + public void RegisterEventHistory(bool registerFinishedOnly = false) { if (level?.LevelData == null) { return; } + level.LevelData.EventsExhausted = !registerFinishedOnly; + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventsExhausted = true; - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab.Identifier).Where(e => !level.LevelData.EventHistory.Contains(e))); + if (registerFinishedOnly) + { + foreach (var finishedEvent in finishedEvents) + { + var key = finishedEvent.ParentSet; + if (key == null) { continue; } + if (level.LevelData.FinishedEvents.ContainsKey(key)) + { + level.LevelData.FinishedEvents[key] += 1; + } + else + { + level.LevelData.FinishedEvents.Add(key, 1); + } + } + } + + level.LevelData.EventHistory.AddRange(selectedEvents.Values + .SelectMany(v => v) + .Select(e => e.Prefab.Identifier) + .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); + if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); + + if (!registerFinishedOnly) + { + level.LevelData.FinishedEvents.Clear(); + } + + bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); } public void SkipEventCooldown() @@ -462,14 +492,14 @@ namespace Barotrauma for (int j = 0; j < eventCount; j++) { if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } - EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; - if (eventPrefabs != null && rand.NextDouble() <= probability) + if (eventPrefabs != null && random.NextDouble() <= probability) { - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); - + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } + newEvent.RandomSeed = randomSeed; if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -483,7 +513,7 @@ namespace Barotrauma } if (eventSet.ChildSets.Any()) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); if (newEventSet != null) { CreateEvents(newEventSet); @@ -494,9 +524,9 @@ namespace Barotrauma { foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { - if (rand.NextDouble() > probability) { continue; } + if (random.NextDouble() > probability) { continue; } - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } if (!selectedEvents.ContainsKey(eventSet)) @@ -783,13 +813,13 @@ namespace Barotrauma { ev.Update(deltaTime); } - else if (ev.Prefab != null && !finishedEvents.Contains(ev.Prefab.Identifier)) + else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } - finishedEvents.Add(ev.Prefab.Identifier); + finishedEvents.Add(ev); } } @@ -833,30 +863,44 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || CharacterParams.CompareGroup(CharacterPrefab.HumanSpeciesName, character.Group)) { continue; } + if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; } - if (!(character.AIController is EnemyAIController enemyAI)) { continue; } - - if (!enemyAI.AIParams.StayInAbyss) + if (character.AIController is EnemyAIController enemyAI) { - // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. - monsterStrength += enemyAI.CombatStrength; + if (!enemyAI.AIParams.StayInAbyss) + { + // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. + monsterStrength += enemyAI.CombatStrength; + } + + if (character.CurrentHull?.Submarine?.Info != null && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && + character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) + { + // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 + enemyDanger += enemyAI.CombatStrength / 500.0f; + } + else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) + { + // Enemy outside targeting the sub or something in it + // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. + enemyDanger += enemyAI.CombatStrength / 5000.0f; + } } - - if (character.CurrentHull?.Submarine?.Info != null && - (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && - character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) + else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { - // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 - enemyDanger += enemyAI.CombatStrength / 500.0f; - } - else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) - { - // Enemy outside targeting the sub or something in it - // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. - enemyDanger += enemyAI.CombatStrength / 5000.0f; + if (character.Submarine != null && + character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } && + Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) + { + //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character), + //so let's just go with a fixed value. + //5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max + enemyDanger += 0.2f; + } } } + // Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub. // On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4. // And if they get inside, we add 0.1 per crawler on that. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 9b7163f83..7bd5e7e01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reflection; +using System.Reflection.Emit; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 276edec6c..b5409cc22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -396,8 +396,16 @@ namespace Barotrauma public int GetEventCount(Level level) { - if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; } - return count; + int finishedEventCount = 0; + if (level is not null) + { + level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount); + } + if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) + { + return eventCount - finishedEventCount; + } + return count - finishedEventCount; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index c0f125362..5da1a323a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -401,7 +401,7 @@ namespace Barotrauma int reward = GetReward(sub); IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier); crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); finalReward = (int)(reward * missionMoneyGainMultiplier.Value); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 1c2663dcf..90e4d007b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -380,11 +380,11 @@ namespace Barotrauma { if (fromType == "any" || fromType == from.Type.Identifier || - (fromType == "anyoutpost" && from.HasOutpost())) + (fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned")) { if (toType == "any" || toType == to.Type.Identifier || - (toType == "anyoutpost" && to.HasOutpost())) + (toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned")) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 278a4d0fe..e849a5499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -260,9 +260,25 @@ namespace Barotrauma int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); for (int i = 0; i < amount; i++) { - Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true); + Vector2 offsetPosition; + int tries = 0; + do + { + offsetPosition = nestPosition + Rand.Vector(100.0f); + tries++; + if (tries > 10) + { + offsetPosition = nestPosition; + break; + } + } while (Level.Loaded.IsPositionInsideWall(offsetPosition)); + Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true); } } + if (Level.Loaded.IsPositionInsideWall(nestPosition)) + { + DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition})."); + } monsterPrefabs.Clear(); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index ecc29fcb6..d52095f5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -19,6 +19,8 @@ namespace Barotrauma private float missionDifficulty; private int alternateReward; + private Identifier factionIdentifier; + private Submarine enemySub; private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); @@ -140,8 +142,8 @@ namespace Barotrauma missionDifficulty = level?.Difficulty ?? 0; XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier); - alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); + factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty); string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } @@ -170,11 +172,11 @@ namespace Barotrauma submarineInfo = new SubmarineInfo(contentFile.Path.Value); } - private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) + private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) { return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble())); } - private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand) + private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand) { return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount); } @@ -254,6 +256,7 @@ namespace Barotrauma } } enemySub.ImmuneToBallastFlora = true; + enemySub.EnableFactionSpecificEntities(factionIdentifier); } private void InitPirates() @@ -444,7 +447,7 @@ namespace Barotrauma private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m)); - private bool DeadOrCaptured(Character character) + private static bool DeadOrCaptured(Character character) { return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 273c38f1c..77e836405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -16,8 +16,8 @@ namespace Barotrauma public Item Item; /// - /// Note that the integer values matter here: the state of the target can't go back to a smaller value, - /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// Note that the integer values matter here: + /// a larger or equal value than the RequiredRetrievalState means the item counts as retrieved /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) /// public enum RetrievalState @@ -167,6 +167,8 @@ namespace Barotrauma private readonly List targets = new List(); + public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get @@ -382,29 +384,55 @@ namespace Barotrauma switch (target.State) { case Target.RetrievalState.None: - if (target.Interacted) { - TrySetRetrievalState(Target.RetrievalState.Interact); - } - var root = target.Item?.GetRootContainer() ?? target.Item; - if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) - { - TrySetRetrievalState(Target.RetrievalState.PickedUp); + if (target.Interacted) + { + TrySetRetrievalState(Target.RetrievalState.Interact); + } + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) + { + TrySetRetrievalState(Target.RetrievalState.PickedUp); + } } + break; case Target.RetrievalState.PickedUp: - Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; - if (parentSub != null && parentSub.Info.Type == SubmarineType.Player) + case Target.RetrievalState.RetrievedToSub: { - TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; + + bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; + bool inPlayerInventory = false; + bool playerInFriendlySub = false; + if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1) + { + inPlayerInventory = true; + if (character.Submarine != null) + { + playerInFriendlySub = + character.IsInFriendlySub || + (character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true }); + } + } + + if (inPlayerSub || (inPlayerInventory && playerInFriendlySub)) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } + else + { + target.State = Target.RetrievalState.PickedUp; + } } break; } void TrySetRetrievalState(Target.RetrievalState retrievalState) { - if (retrievalState < target.State) { return; } - bool wasRetrieved = false; + if (retrievalState < target.State || target.State == retrievalState) { return; } + bool wasRetrieved = target.Retrieved; target.State = retrievalState; //increment the mission state if the target became retrieved if (!wasRetrieved && target.Retrieved) { State = i + 1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d4e57fc47..e45b7b4cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -244,7 +244,12 @@ namespace Barotrauma float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + if (sub.Info.Type != SubmarineType.Player && + sub.Info.Type != SubmarineType.EnemySubmarine && + sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) + { + continue; + } float minDistToSub = GetMinDistanceToSub(sub); if (dist < minDistToSub * minDistToSub) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 1a741f3b7..0560da516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -260,10 +260,12 @@ namespace Barotrauma } bool success = false; bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; + float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) { if (preferredContainer.CampaignOnly && !isCampaign) { continue; } if (preferredContainer.NotCampaign && isCampaign) { continue; } + if (levelDifficulty < preferredContainer.MinLevelDifficulty || levelDifficulty > preferredContainer.MaxLevelDifficulty) { continue; } if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; } validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true); if (validContainers.None()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 03839f12f..a397da922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -420,20 +420,18 @@ namespace Barotrauma { List availableSpeakers = new List() { npc, player }; List dialogFlags = new List() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() }; + if (npc.HumanPrefab != null) + { + foreach (var tag in npc.HumanPrefab.GetTags()) + { + dialogFlags.Add(tag); + } + } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned") { - if (npc.TeamID == CharacterTeamType.None) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Bandit".ToIdentifier()); - } - else if (npc.TeamID == CharacterTeamType.FriendlyNPC) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Hostage".ToIdentifier()); - } + dialogFlags.Remove("OutpostNPC".ToIdentifier()); } else if (campaignMode.Map?.CurrentLocation?.Reputation != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 5129e6a81..50e9ff216 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,16 +1,26 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { class Reputation { public const float HostileThreshold = 0.2f; - public const float ReputationLossPerNPCDamage = 0.05f; - public const float ReputationLossPerWallDamage = 0.05f; - public const float ReputationLossPerStolenItemPrice = 0.005f; - public const float MinReputationLossPerStolenItem = 0.05f; - public const float MaxReputationLossPerStolenItem = 1.0f; + public const float ReputationLossPerNPCDamage = 0.025f; + public const float ReputationLossPerWallDamage = 0.025f; + public const float ReputationLossPerStolenItemPrice = 0.0025f; + public const float MinReputationLossPerStolenItem = 0.025f; + public const float MaxReputationLossPerStolenItem = 0.5f; + + /// + /// Maximum amount of reputation loss you can get from damaging outpost NPCs per round + /// + public const float MaxReputationLossFromNPCDamage = 20.0f; + /// + /// Maximum amount of reputation loss you can get from damaging outpost walls per round + /// + public const float MaxReputationLossFromWallDamage = 10.0f; public Identifier Identifier { get; } public int MinReputation { get; } @@ -18,6 +28,8 @@ namespace Barotrauma public int InitialReputation { get; } public CampaignMetadata Metadata { get; } + public float ReputationAtRoundStart { get; set; } + private readonly Identifier metaDataIdentifier; /// @@ -60,32 +72,50 @@ namespace Barotrauma public float GetReputationChangeMultiplier(float reputationChange) { - if (reputationChange > 0f) + return reputationChange switch { - float reputationGainMultiplier = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); - reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; - } - return reputationGainMultiplier; - } - else if (reputationChange < 0f) + > 0f => GetMultiplierForStatType(StatTypes.ReputationGainMultiplier, Identifier), + < 0f => GetMultiplierForStatType(StatTypes.ReputationLossMultiplier, Identifier), + _ => 1.0f + }; + + static float GetMultiplierForStatType(StatTypes statTypes, Identifier identifier) { - float reputationLossMultiplier = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + float multiplier = 1f; + var crew = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (crew != null && crew.Any()) { - reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); - reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; + multiplier *= 1f + crew.Max(c => c.GetStatValue(statTypes, includeSaved: false)); + multiplier *= 1f + crew.Max(c => c.Info?.GetSavedStatValue(statTypes, identifier) ?? 0); } - return reputationLossMultiplier; + return multiplier; } - return 1.0f; } - public void AddReputation(float reputationChange) + public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue) { - Value += reputationChange * GetReputationChangeMultiplier(reputationChange); + float prevValue = Value; + //if we're already over the limit, do nothing (assuming the change is in the "same direction" that we've gone over the limit) + if (doesReputationChangeGoOverLimit(prevValue, reputationChange)) + { + return; + } + + float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange); + if (doesReputationChangeGoOverLimit(newValue, newValue - prevValue)) + { + newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange); + } + + Value = newValue; + + bool doesReputationChangeGoOverLimit(float newValue, float change) + { + float totalReputationChange = newValue - ReputationAtRoundStart; + return + Math.Abs(totalReputationChange) > maxReputationChangePerRound && + Math.Sign(totalReputationChange) == Math.Sign(change); + } } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); @@ -114,6 +144,7 @@ namespace Barotrauma metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier(); MinReputation = minReputation; MaxReputation = maxReputation; + ReputationAtRoundStart = initialReputation; InitialReputation = initialReputation; Faction = faction; Location = location; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9db5d70b8..85535d130 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -105,24 +105,30 @@ namespace Barotrauma { get { - if (Map.CurrentLocation != null) + //map can be null if we're in the process of loading the save + if (Map != null) { - foreach (Mission mission in map.CurrentLocation.SelectedMissions) + if (Map.CurrentLocation != null) { - if (mission.Locations[0] == mission.Locations[1] || - mission.Locations.Contains(Map.SelectedLocation)) + foreach (Mission mission in map.CurrentLocation.SelectedMissions) { - yield return mission; + if (mission.Locations[0] == mission.Locations[1] || + mission.Locations.Contains(Map.SelectedLocation)) + { + yield return mission; + } } } - } - foreach (Mission mission in extraMissions) - { - yield return mission; + foreach (Mission mission in extraMissions) + { + yield return mission; + } } } } + public Location CurrentLocation => Map?.CurrentLocation; + public Wallet Bank; public LevelData NextLevel @@ -247,6 +253,11 @@ namespace Barotrauma prevCampaignUIAutoOpenType = TransitionType.None; #endif + foreach (var faction in factions) + { + faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value; + } + if (PurchasedHullRepairsInLatestSave) { foreach (Structure wall in Structure.WallList) @@ -907,6 +918,10 @@ namespace Barotrauma { location.TurnsInRadiation = 0; } + foreach (var faction in Factions) + { + faction.Reputation.SetReputation(faction.Prefab.InitialReputation); + } EndCampaignProjSpecific(); if (CampaignMetadata != null) @@ -1154,15 +1169,12 @@ namespace Barotrauma if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction) { - faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); + faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); } else { Location location = Map?.CurrentLocation; - if (location != null) - { - location.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); - } + location?.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 4d0a5186a..a2a9ea63b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -11,6 +11,9 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private const int RateLimitMaxRequests = 20, + RateLimitExpiry = 5; + public enum NetworkHeader { REQUEST_AFFLICTIONS, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index d644c8267..90eb890d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -24,7 +24,7 @@ namespace Barotrauma } - public static readonly List anySlot = new List() { InvSlotType.Any }; + public static readonly List AnySlot = new List() { InvSlotType.Any }; protected bool[] IsEquipped; @@ -226,7 +226,7 @@ namespace Barotrauma if (item.AllowedSlots.Contains(InvSlotType.Any)) { var wearable = item.GetComponent(); - if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1) + if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item)) { return false; } @@ -336,7 +336,7 @@ namespace Barotrauma //try to place the item in a LimbSlot.Any slot if that's allowed if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any)) { - int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot); + int freeIndex = GetFreeAnySlot(item, inWrongSlot); if (freeIndex > -1) { PutItem(item, freeIndex, user, true, createNetworkEvent); @@ -393,7 +393,9 @@ namespace Barotrauma return placedInSlot > -1; } - public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot) + public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1; + + private int GetFreeAnySlot(Item item, bool inWrongSlot) { //attempt to stack first for (int i = 0; i < capacity; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 3c4600b95..ccf437103 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -561,6 +561,12 @@ namespace Barotrauma.Items.Components gap.ConnectedDoor = null; } } + + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture); + OutsideSubmarineFixture = null; + } //no need to remove the gap if we're unloading the whole submarine //otherwise the gap will be removed twice and cause console warnings diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index ff4f8b21b..b039709e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -600,7 +600,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - //no further data needed, the event just triggers the discharge + msg.WriteUInt16(user?.ID ?? Entity.NullEntityID); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 6620a203c..5890b1ec9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -700,7 +700,8 @@ namespace Barotrauma.Items.Components } Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true); Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine; - int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int currentlyAttachedCount = Item.ItemList.Count( i => i.Submarine == attachSubmarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index cf93558ef..950e8cfae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter); + float spread = GetSpread(character) * projectile.GetSpreadFromPool(); var lastProjectile = LastProjectile; if (lastProjectile != projectile) @@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter)); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool()); } Item.RemoveContained(projectile.Item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index fd24d9673..bbee3559e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] public bool RepairMultiple { get; set; } + [Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")] + public bool RepairMultipleWalls { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } @@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components //stop the ray if it already hit a door/wall and is now about to hit some other type of entity if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; } } + if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent() != null)) { break; } Character hitCharacter = null; if (body.UserData is Limb limb) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 50f5fc39f..3dacee1f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -236,7 +236,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")] public float CombatPriority { get; private set; } /// @@ -687,9 +687,10 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } - public bool IsNotEmpty(Character user, bool checkContainedItems = true) => - HasRequiredContainedItems(user, addMessage: false) && - (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); + /// + /// Shorthand for !HasRequiredContainedItems() + /// + public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false); public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 555611027..548cc0319 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -511,25 +511,28 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; + targets.Clear(); + bool wearing = item.GetComponent() is Wearable { IsActive: true }; if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); + targets.AddRange(item.AllPropertyObjects); } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { - effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + targets.AddRange(contained.AllPropertyObjects); } if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) { - effect.Apply(ActionType.OnContaining, deltaTime, item, character); + targets.Add(character); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - targets.Clear(); effect.AddNearbyTargets(item.WorldPosition, targets); - effect.Apply(ActionType.OnActive, deltaTime, item, targets); } + effect.Apply(ActionType.OnActive, deltaTime, item, targets); + effect.Apply(ActionType.OnContaining, deltaTime, item, targets); + if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0c41133e9..f8f3df86e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components itemList.Enabled = true; if (amountInput != null) { - amountInput.Enabled = true; + amountInput.Enabled = amountTextMax.Enabled; } RefreshActivateButtonText(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b7b475055..dfc4db02a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -14,19 +14,18 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { - const int SpreadCounterWrapAround = 256; - private static readonly ImmutableArray spreadPool; static Projectile() { MTRandom random = new MTRandom(0); - spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); } - public static float GetSpreadFromPool(int seed) + public static byte SpreadCounter { get; private set; } + + public static void ResetSpreadCounter() { - if (seed < 0) { seed = -seed; } - return spreadPool[seed % SpreadCounterWrapAround]; + SpreadCounter = 0; } struct HitscanResult @@ -63,7 +62,7 @@ namespace Barotrauma.Items.Components private bool removePending; - public byte SpreadCounter { get; private set; } + private byte spreadIndex; //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -212,7 +211,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")] public bool StaticSpread { get; @@ -300,7 +299,8 @@ namespace Barotrauma.Items.Components return; } - SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + spreadIndex = SpreadCounter; + SpreadCounter++; InitProjSpecific(element); } @@ -329,6 +329,12 @@ namespace Barotrauma.Items.Components originalCollisionTargets = item.body.CollidesWith; } + public float GetSpreadFromPool() + { + spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length); + return spreadPool[spreadIndex]; + } + private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f) { if (Item.body == null) { return; } @@ -380,8 +386,8 @@ namespace Barotrauma.Items.Components if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER - launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); + launchRot = rotation; + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1))); #endif } } @@ -390,24 +396,22 @@ namespace Barotrauma.Items.Components { if (character != null && !characterUsable) { return false; } if (item.body == null) { return false; } + //can't launch if already launched + if (StickTarget != null || IsActive) { return false; } + float initialRotation = item.body.Rotation; for (int i = 0; i < HitScanCount; i++) { float launchAngle; - if (StaticSpread) { - float staticSpread = Spread / (HitScanCount - 1); - // because the position of the item changes as hitscan are fired, we will set an - // initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired - float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f; - launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset); + launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread; } else { - launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter)); + launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool()); } - SpreadCounter++; + spreadIndex++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index ceced8a5b..eb76f17ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -243,14 +243,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < labels.Length; i++) { labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; - if (Screen.Selected != GameMain.SubEditorScreen) - { - customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value; - } - else - { - customInterfaceElementList[i].Label = labels[i]; - } + customInterfaceElementList[i].Label = labels[i]; } UpdateLabelsProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index a2dcaacfa..097d23817 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components Position = item.Position, CastShadows = castShadows, IsBackground = drawBehindSubs, - SpriteScale = Vector2.One * item.Scale, + SpriteScale = Vector2.One * item.Scale * LightSpriteScale, Range = range }; Light.LightSourceParams.Flicker = flicker; @@ -309,13 +309,14 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null && item.GetRootInventoryOwner() is not Character) + var ownerCharacter = item.GetRootInventoryOwner() as Character; + if ((item.Container != null && ownerCharacter == null) || + (ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f)) { lightBrightness = 0.0f; SetLightSourceState(false, 0.0f); return; } - SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 8455a3b14..c6ea9bc03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components private float resetUserTimer; - private float aiTargetingGraceTimer; - private float aiFindTargetTimer; private ISpatialEntity currentTarget; - private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAiFindTargetMaxInterval = 1.0f; private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -299,7 +297,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] + [Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] public float AIRange { get; @@ -422,10 +420,7 @@ namespace Barotrauma.Items.Components // Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it. if (lc?.Parent == this) { - if (lightComponents == null) - { - lightComponents = new List(); - } + lightComponents ??= new List(); lightComponents.Add(lc); } } @@ -439,6 +434,8 @@ namespace Barotrauma.Items.Components light.Parent = null; light.Rotation = Rotation - item.RotationRad; light.Light.Rotation = -rotation; + //turret lights are high-prio (don't want the lights to disappear when you're fighting something) + light.Light.PriorityMultiplier *= 10.0f; } } #endif @@ -590,10 +587,6 @@ namespace Barotrauma.Items.Components angularVelocity *= -0.5f; } - if (aiTargetingGraceTimer > 0f) - { - aiTargetingGraceTimer -= deltaTime; - } if (aiFindTargetTimer > 0.0f) { aiFindTargetTimer -= deltaTime; @@ -620,10 +613,18 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime); + private bool isUseBeingCalled; + public override bool Use(float deltaTime, Character character = null) { if (!characterUsable && character != null) { return false; } - return TryLaunch(deltaTime, character); + //prevent an infinite loop if launching triggers a StatusEffect that Uses this item + if (isUseBeingCalled) { return false; } + + isUseBeingCalled = true; + bool wasSuccessful = TryLaunch(deltaTime, character); + isUseBeingCalled = false; + return wasSuccessful; } public float GetPowerRequiredToShoot() @@ -1015,6 +1016,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (dist > closestDist) { continue; } if (dist > shootDistance * shootDistance) { continue; } + if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } target = targetItem; closestDist = dist / priority; @@ -1130,9 +1132,12 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead").Value, - identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), - minDurationBetweenSimilar: 10.0f); + if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character) + { + character.Speak(TextManager.Get("DialogTurretTargetDead").Value, + identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), + minDurationBetweenSimilar: 5.0f); + } character.AIController.SelectTarget(null); } @@ -1265,18 +1270,27 @@ namespace Barotrauma.Items.Components Vector2? targetPos = null; float maxDistance = 10000; float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; - // use full range only if we're actively firing - if (aiTargetingGraceTimer <= 0f) - { - shootDistance *= 0.75f; - } - float closestDistance = maxDistance * maxDistance; - bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (!IsValidTarget(currentTarget)) + bool isValidTarget = IsValidTarget(currentTarget); + if (isValidTarget) + { + float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition); + if (dist > closestDistance) + { + isValidTarget = false; + } + else if (currentTarget is Item targetItem) + { + if (!IsTargetItemCloseEnough(targetItem, dist)) + { + isValidTarget = false; + } + } + } + if (!isValidTarget) { currentTarget = null; aiFindTargetTimer = CrewAIFindTargetMinInverval; @@ -1292,7 +1306,11 @@ namespace Barotrauma.Items.Components if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } - if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.Submarine != null) + { + if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.Submarine.Info.IsOutpost) { continue; } + } } // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } @@ -1318,6 +1336,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (dist > closestDistance) { continue; } if (dist > shootDistance * shootDistance) { continue; } + if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } targetPos = targetItem.WorldPosition; closestDistance = dist / priority; @@ -1325,14 +1344,7 @@ namespace Barotrauma.Items.Components closestEnemy = null; currentTarget = targetItem; } - if (currentTarget == null) - { - aiFindTargetTimer = CrewAIFindTargetMinInverval; - } - else - { - aiFindTargetTimer = CrewAiFindTargetMaxInterval; - } + aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval; } else if (currentTarget != null) { @@ -1346,6 +1358,10 @@ namespace Barotrauma.Items.Components if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) { targetPos = targetCharacter.CurrentHull.WorldPosition; + if (closestDistance > maxDistance * maxDistance) + { + ResetTarget(); + } } else { @@ -1365,12 +1381,17 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot. - currentTarget = null; - closestEnemy = null; - targetPos = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; + ResetTarget(); } } + void ResetTarget() + { + // Not close enough to shoot. + currentTarget = null; + closestEnemy = null; + targetPos = null; + } } else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { @@ -1520,10 +1541,11 @@ namespace Barotrauma.Items.Components } character.SetInput(InputType.Shoot, true, true); } - aiTargetingGraceTimer = 5f; return false; } + private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance); + /// /// Turret doesn't consume grid power, directly takes from the batteries on its grid instead. /// @@ -1553,6 +1575,10 @@ namespace Barotrauma.Items.Components { return false; } + if (targetItem.ParentInventory != null) + { + return false; + } } return true; } @@ -1568,6 +1594,7 @@ namespace Barotrauma.Items.Components { if (item.Submarine != null) { + if (item.Submarine.Info.IsOutpost) { return false; } // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; } @@ -1660,7 +1687,7 @@ namespace Barotrauma.Items.Components return angle >= minRotation && angle <= maxRotation; } - private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); protected override void RemoveComponentSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index fc8e6dd4d..c5a73d53f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")] public bool AllowUseWhenWorn { get; set; } public readonly int Variants; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 5a08c3838..7ee77aca8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -742,13 +742,13 @@ namespace Barotrauma stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)) && (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || - existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)); + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent)); } else { swapSuccessful = (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || - existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)) + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent)) && stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 301af1341..f24dd2f6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -651,11 +651,13 @@ namespace Barotrauma } } - [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] - public bool AllowStealing + private bool allowStealing; + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true, + description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")] + public bool AllowStealing { - get; - set; + get { return allowStealing || Prefab.AllowStealingAlways; } + set { allowStealing = value; } } private string originalOutpost; @@ -1585,6 +1587,17 @@ namespace Barotrauma return tags.Contains(tag) || base.Prefab.Tags.Contains(tag); } + public bool HasIdentifierOrTags(IEnumerable identifiersOrTags) + { + if (identifiersOrTags == null) { return false; } + if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; } + foreach (Identifier tag in identifiersOrTags) + { + if (HasTag(tag)) { return true; } + } + return false; + } + public void ReplaceTag(string tag, string newTag) { ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier()); @@ -1756,7 +1769,7 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - private void SetCondition(float value, bool isNetworkEvent) + private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true) { if (!isNetworkEvent) { @@ -1779,16 +1792,22 @@ namespace Barotrauma //Flag connections to be updated as device is broken flagChangedConnections(connections); #if CLIENT - foreach (ItemComponent ic in components) - { - ic.PlaySound(ActionType.OnBroken); - ic.StopSounds(ActionType.OnActive); + if (executeEffects) + { + foreach (ItemComponent ic in components) + { + ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); + } } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow. SetPreviousCondition(); - ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + if (executeEffects) + { + ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + } } else if (condition > 0.0f && prevCondition <= 0.0f) { @@ -1872,15 +1891,15 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - CreateStatusEvent(); + CreateStatusEvent(loadingRound: false); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } - public void CreateStatusEvent() + public void CreateStatusEvent(bool loadingRound) { - GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound)); } private bool isActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 4ad6b2731..010a675b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -71,9 +71,9 @@ namespace Barotrauma { public EventType EventType => EventType.ItemStat; - public readonly Dictionary Stats; + public readonly Dictionary Stats; - public SetItemStatEventData(Dictionary stats) + public SetItemStatEventData(Dictionary stats) { Stats = stats; } @@ -82,6 +82,13 @@ namespace Barotrauma private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; + + public readonly bool LoadingRound; + + public ItemStatusEventData(bool loadingRound) + { + LoadingRound = loadingRound; + } } private readonly struct AssignCampaignInteractionEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index a4a8fc521..b6ceab82c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -332,6 +332,8 @@ namespace Barotrauma public readonly bool TransferOnlyOnePerContainer; public readonly bool AllowTransfersHere = true; + public readonly float MinLevelDifficulty, MaxLevelDifficulty; + public PreferredContainer(XElement element) { Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); @@ -347,6 +349,9 @@ namespace Barotrauma TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); + MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue); + MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue); + if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 @@ -670,6 +675,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool AllowSellingWhenBroken { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AllowStealingAlways { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool Indestructible { get; private set; } @@ -806,6 +814,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] public float AISlowTurretPriority { get; private set; } + [Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")] + public float AITurretTargetingMaxDistance { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs index c6091915f..ba7aef6a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -5,29 +5,48 @@ using System.Collections.Generic; namespace Barotrauma { + [NetworkSerialize] + internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId) : INetSerializableStruct + { + /// + /// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist. + /// + public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId) + => new(stat, talentIdentifier, Option.Some(characterId)); + + /// + /// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied. + /// will always use the highest value for unstackable stats. + /// + public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier) + => new(stat, talentIdentifier, Option.None); + } + internal sealed class ItemStatManager { - private Item item; - - public ItemStatManager(Item item) - { - this.item = item; - } - - [NetworkSerialize] - public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct; - private readonly Dictionary talentStats = new(); + private readonly Item item; - public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent) + public ItemStatManager(Item item) => this.item = item; + + public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent) { if (talent.Character?.ID is not { } characterId || - talent.Prefab?.Identifier is not { } talentIdentifier) + talent.Prefab?.Identifier is not { } talentIdentifier) { return; } + + var identifier = stackable + ? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId) + : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier); + + if (!stackable) { - return; + if (talentStats.TryGetValue(identifier, out float existingValue)) + { + // Always use the highest value for non-stackable stats + if (existingValue > value) { return; } + } } - TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId); talentStats[identifier] = value; #if SERVER @@ -38,21 +57,19 @@ namespace Barotrauma #endif } - // Used for getting the value value from network packet - public void ApplyStat(TalentStatIdentifier identifier, float value) - { - talentStats[identifier] = value; - } + /// + /// Used for setting the value value from network packet; bypassing all validity checks. + /// + public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value; public float GetAdjustedValue(ItemTalentStats stat, float originalValue) { float total = originalValue; + foreach (var (key, value) in talentStats) { - if (key.Stat == stat) - { - total *= value; - } + if (key.Stat != stat) { continue; } + total *= value; } return total; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 11e69f606..30c88bb93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -8,44 +8,85 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Used by various features to define different kinds of relations between items: + /// for example, which item a character must have equipped to interact with some item in some way, + /// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute. + /// class RelatedItem { public enum RelationType { None, + /// + /// The item must be contained inside the item this relation is defined in. + /// Can for example by used to make an item usable only when there's a specific kind of item inside it. + /// Contained, + /// + /// The user must have equipped the item (i.e. held or worn). + /// Equipped, + /// + /// The user must have picked up the item (i.e. the item needs to be in the user's inventory). + /// Picked, + /// + /// The item this relation is defined in must be inside a specific kind of container. + /// Can for example by used to make an item do something when it's inside some other type of item. + /// Container } - public bool IsOptional { get; set; } - + /// + /// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. + /// public bool MatchOnEmpty { get; set; } + /// + /// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. + /// public bool RequireEmpty { get; set; } + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor, + /// making it easier to for example make rewire things that require some special tool to rewire. + /// public bool IgnoreInEditor { get; set; } + /// + /// Identifier(s) or tag(s) of the items that are NOT considered valid. + /// Can be used to, for example, exclude some specific items when using tags that apply to multiple items. + /// public ImmutableHashSet ExcludedIdentifiers { get; private set; } private RelationType type; public List statusEffects; - + + /// + /// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public LocalizedString Msg; + + /// + /// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public Identifier MsgTag; /// - /// Should broken (0 condition) items be excluded + /// Should broken (0 condition) items be excluded? /// public bool ExcludeBroken { get; private set; } /// - /// Should full condition (100%) items be excluded + /// Should full condition (100%) items be excluded? /// public bool ExcludeFullCondition { get; private set; } + /// + /// Are item variants considered valid? + /// public bool AllowVariants { get; private set; } = true; public RelationType Type @@ -59,19 +100,34 @@ namespace Barotrauma public int TargetSlot = -1; /// - /// Overrides the position defined in ItemContainer. + /// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer. /// public Vector2? ItemPos; /// - /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. /// public bool Hide; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to override the rotation of specific items in the container. + /// public float Rotation; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun). + /// public bool SetActive; + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional, + /// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door). + /// + public bool IsOptional { get; set; } + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -83,6 +139,9 @@ namespace Barotrauma } } + /// + /// Identifier(s) or tag(s) of the items that are considered valid. + /// public ImmutableHashSet Identifiers { get; private set; } public string JoinedExcludedIdentifiers diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 59721d257..b1f0392c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -290,6 +290,7 @@ namespace Barotrauma dictionary.Clear(); Hull.EntityGrids.Clear(); Spawner?.Reset(); + Items.Components.Projectile.ResetSpreadCounter(); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 2ce9bd0e8..e7bd78a35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -10,37 +10,169 @@ using System.Linq; namespace Barotrauma { + /// + /// Explosions are area of effect attacks that can damage characters, items and structures. + /// + /// + /// + /// Used to enable all particle effects without having to specify them one by one. + /// + /// partial class Explosion { public readonly Attack Attack; + /// + /// How much force the explosion applies to the characters. + /// private readonly float force; - private readonly float cameraShake, cameraShakeRange; + /// + /// Intensity of the screen shake effect. + /// + /// + /// + /// 10% of the range if showEffects is true, 0 otherwise. + /// + /// + private readonly float cameraShake; + /// + /// How far away does the camera shake effect reach. + /// + /// + /// + /// Same as attack range if showEffects is true, 0 otherwise. + /// + /// + private readonly float cameraShakeRange; + + /// + /// Color tint to apply to the player's screen when in range of the explosion. + /// private readonly Color screenColor; - private readonly float screenColorRange, screenColorDuration; - private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; + /// + /// How far away can the screen color effect be seen. + /// + /// + /// + /// 10% of the range if showEffects is true, 0 otherwise. + /// + /// + private readonly float screenColorRange; + + /// + /// How long the screen color effect lasts. + /// + private readonly float screenColorDuration; + + /// + /// Whether a spark particle effect is created when the explosion happens. + /// + private bool sparks; + + /// + /// Whether a shockwave particle effect is created when the explosion happens. + /// + private bool shockwave; + + /// + /// Whether a flame particle effect is created when the explosion happens. + /// + private bool flames; + + /// + /// Whether a smoke particle effect is created when the explosion happens. + /// + private bool smoke; + + /// + /// Whether a flash effect is created when the explosion happens. + /// + private bool flash; + + /// + /// Whether a underwater bubble particle effect is created when the explosion happens. + /// + private bool underwaterBubble; + + /// + /// Color of the light source created by the explosion. + /// private readonly Color flashColor; + + /// + /// Whether the explosion plays a tinnitus sound to players who get hit by it. + /// private readonly bool playTinnitus; + + /// + /// Whether the explosion executes 'OnFire' status effects on the items it hits. + /// + /// + /// + /// true if showEffects is true and flames haven't been explicitly set to false, false otherwise. + /// + /// private readonly bool applyFireEffects; + + /// + /// List of item tags that the explosion ignores when applying fire effects. + /// private readonly string[] ignoreFireEffectsForTags; + + /// + /// When set to true, the explosion don't deal less damage when the target is behind a solid object. + /// private readonly bool ignoreCover; + + /// + /// How long the light source created by the explosion lasts. + /// private readonly float flashDuration; + + /// + /// How large the light source created by the explosion is. + /// private readonly float? flashRange; + + /// + /// Identifier of the decal the explosion creates on the background structure it explodes over. + /// Set to empty string to disable. + /// private readonly string decal; + + /// + /// Relative size of the decal created by the explosion. + /// private readonly float decalSize; - private readonly bool applyToSelf; - public bool OnlyInside, OnlyOutside; + /// + /// Whether the explosion only affects characters inside a submarine. + /// + public bool OnlyInside; + /// + /// Whether the explosion only affects characters outside a submarine. + /// + public bool OnlyOutside; + + /// + /// How much the explosion repairs items. + /// private readonly float itemRepairStrength; public readonly HashSet IgnoredSubmarines = new HashSet(); + /// + /// Strength of the EMP effect created by the explosion. + /// public float EmpStrength { get; set; } - + + /// + /// How much damage the explosion does to ballast flora. + /// public float BallastFloraDamage { get; set; } public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f) @@ -66,8 +198,6 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - applyToSelf = element.GetAttributeBool("applytoself", true); - //the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects") bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true); sparks = element.GetAttributeBool("sparks", showEffects); @@ -231,7 +361,7 @@ namespace Barotrauma return; } - DamageCharacters(worldPosition, Attack, force, damageSource, attacker, applyToSelf); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -284,8 +414,8 @@ namespace Barotrauma } partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - - private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, bool applyToSelf) + + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } @@ -300,7 +430,6 @@ namespace Barotrauma { continue; } - //if (c == attacker && !applyToSelf) { continue; } if (OnlyInside && c.Submarine == null) { @@ -513,21 +642,28 @@ namespace Barotrauma for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; } + + bool inRange = false; foreach (var cell in destructibleWall.Cells) { if (cell.IsPointInside(worldPosition)) { - destructibleWall.AddDamage(levelWallDamage, worldPosition); - continue; + inRange = true; + break; } foreach (var edge in cell.Edges) { if (MathUtils.LineSegmentToPointDistanceSquared((edge.Point1 + cell.Translation).ToPoint(), (edge.Point2 + cell.Translation).ToPoint(), worldPosition.ToPoint()) < worldRange * worldRange) { - destructibleWall.AddDamage(levelWallDamage, worldPosition); + inRange = true; break; } } + if (inRange) { break; } + } + if (inRange) + { + destructibleWall.AddDamage(levelWallDamage, worldPosition); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 112f8d5bd..ddd99a9d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -9,7 +9,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class Gap : MapEntity + partial class Gap : MapEntity, ISerializableEntity { public static List GapList = new List(); @@ -57,6 +57,8 @@ namespace Barotrauma private Body outsideCollisionBlocker; private float outsideColliderRaycastTimer; + private bool wasRoomToRoom; + public float Open { get { return open; } @@ -153,12 +155,12 @@ namespace Barotrauma } } - public override string Name + public override string Name => "Gap"; + + public readonly Dictionary properties; + public Dictionary SerializableProperties { - get - { - return "Gap"; - } + get { return properties; } } public Gap(Rectangle rectangle) @@ -185,6 +187,8 @@ namespace Barotrauma IsDiagonal = isDiagonal; open = 1.0f; + properties = SerializableProperty.GetProperties(this); + FindHulls(); GapList.Add(this); InsertToList(); @@ -199,7 +203,10 @@ namespace Barotrauma outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; -#endif +# endif + + wasRoomToRoom = IsRoomToRoom; + RefreshOutsideCollider(); DebugConsole.Log("Created gap (" + ID + ")"); } @@ -332,9 +339,14 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { flowForce = Vector2.Zero; - outsideColliderRaycastTimer -= deltaTime; + if (IsRoomToRoom != wasRoomToRoom) + { + RefreshOutsideCollider(); + wasRoomToRoom = IsRoomToRoom; + } + if (open == 0.0f || linkedTo.Count == 0) { lerpedFlowForce = Vector2.Zero; @@ -628,11 +640,16 @@ namespace Barotrauma public bool RefreshOutsideCollider() { - if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false; + if (outsideCollisionBlocker == null) { return false; } + if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) + { + outsideCollisionBlocker.Enabled = false; + return false; + } if (outsideColliderRaycastTimer <= 0.0f) { - UpdateOutsideColliderPos((Hull)linkedTo[0]); + UpdateOutsideColliderState((Hull)linkedTo[0]); outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ? OutsideColliderRaycastIntervalHighPrio : OutsideColliderRaycastIntervalLowPrio; @@ -641,7 +658,7 @@ namespace Barotrauma return outsideCollisionBlocker.Enabled; } - private void UpdateOutsideColliderPos(Hull hull) + private void UpdateOutsideColliderState(Hull hull) { if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; } @@ -678,7 +695,7 @@ namespace Barotrauma if (blockingBody.UserData == Submarine) { return; } outsideCollisionBlocker.Enabled = true; Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition; - float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2; + float colliderRotation = MathUtils.VectorToAngle(Submarine.LastPickedNormal) - MathHelper.PiOver2; outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); } else @@ -775,8 +792,7 @@ namespace Barotrauma public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { - Rectangle rect = Rectangle.Empty; - + Rectangle rect; if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index c80309932..b2aaf0964 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -65,28 +65,28 @@ namespace Barotrauma set { maxHeight = Math.Max(value, minHeight); } } - [Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(2, IsPropertySaveable.Yes, description: "Minimum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinBranchCount { get { return minBranchCount; } set { minBranchCount = Math.Max(value, 0); } } - [Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(4, IsPropertySaveable.Yes, description: "Maximum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxBranchCount { get { return maxBranchCount; } set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(50, IsPropertySaveable.Yes, description: "Total amount of level objects in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int LevelObjectAmount { get; set; } - [Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "What portion of the empty cells in the cave should be turned into destructible walls? For example, 0.1 = 10%."), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] public float DestructibleWallRatio { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index a6675f4a3..7427002f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -429,7 +429,7 @@ namespace Barotrauma public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost; /// - /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1) + /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reputation into account. /// public static bool IsLoadedFriendlyOutpost => loaded?.Type == LevelData.LevelType.Outpost && @@ -468,8 +468,7 @@ namespace Barotrauma (StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) { - var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation; - return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold; + return GameMain.GameSession.Campaign?.CurrentLocation is not { IsFactionHostile: true }; } return false; } @@ -508,6 +507,8 @@ namespace Barotrauma StartLocation = startLocation; EndLocation = endLocation; + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); @@ -535,7 +536,6 @@ namespace Barotrauma List sites = new List(); Voronoi voronoi = new Voronoi(1.0); - Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); #if CLIENT renderer = new LevelRenderer(this); @@ -838,14 +838,6 @@ namespace Barotrauma foreach (var pathCell in tunnel.Cells) { MarkEdges(pathCell, tunnel.Type); - foreach (GraphEdge edge in pathCell.Edges) - { - var adjacent = edge.AdjacentCell(pathCell); - if (adjacent != null) - { - MarkEdges(adjacent, tunnel.Type); - } - } if (!pathCells.Contains(pathCell)) { pathCells.Add(pathCell); @@ -1813,6 +1805,7 @@ namespace Barotrauma if (AbyssArea.Height < islandSize.Y) { return; } + int createdCaves = 0; int islandCount = GenerationParams.AbyssIslandCount; for (int i = 0; i < islandCount; i++) { @@ -1842,8 +1835,13 @@ namespace Barotrauma break; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability) + bool createCave = + //force at least one abyss cave + (i == islandCount - 1 && createdCaves == 0) || + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < GenerationParams.AbyssIslandCaveProbability; + if (!createCave) { + //just create a chunk with no cave float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); Vector2 position = islandArea.Center.ToVector2(); @@ -1901,6 +1899,7 @@ namespace Barotrauma new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2), new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland))); AbyssIslands.Add(new AbyssIsland(islandArea, islandCells)); + createdCaves++; } } @@ -3013,10 +3012,12 @@ namespace Barotrauma if (PositionsOfInterest.None(p => p.PositionType == positionType)) { + DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources."); foreach (var validType in MineralMission.ValidPositionTypes) { if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType)) { + DebugConsole.AddWarning($"Placing in \"{validType}\" instead."); positionType = validType; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 090259ff9..b7a5aea58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -67,6 +67,8 @@ namespace Barotrauma /// public readonly List NonRepeatableEvents = new List(); + public readonly Dictionary FinishedEvents = new Dictionary(); + /// /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. . /// @@ -155,6 +157,47 @@ namespace Barotrauma string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); + string finishedEventsName = nameof(FinishedEvents); + if (element.GetChildElement(finishedEventsName) is { } finishedEventsElement) + { + foreach (var childElement in finishedEventsElement.GetChildElements(finishedEventsName)) + { + Identifier eventSetIdentifier = childElement.GetAttributeIdentifier("set", Identifier.Empty); + if (eventSetIdentifier.IsEmpty) { continue; } + if (!EventSet.Prefabs.TryGet(eventSetIdentifier, out EventSet eventSet)) + { + foreach (var prefab in EventSet.Prefabs) + { + if (FindSetRecursive(prefab, eventSetIdentifier) is { } foundSet) + { + eventSet = foundSet; + break; + } + } + } + if (eventSet is null) { continue; } + int count = childElement.GetAttributeInt("count", 0); + if (count < 1) { continue; } + FinishedEvents.Add(eventSet, count); + } + + static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) + { + foreach (var childSet in parentSet.ChildSets) + { + if (childSet.Identifier == setIdentifier) + { + return childSet; + } + if (FindSetRecursive(childSet, setIdentifier) is { } foundSet) + { + return foundSet; + } + } + return null; + } + } + EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } @@ -319,6 +362,18 @@ namespace Barotrauma { newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents))); } + if (FinishedEvents.Any()) + { + var finishedEventsElement = new XElement(nameof(FinishedEvents)); + foreach (var (set, count) in FinishedEvents) + { + var element = new XElement(nameof(FinishedEvents), + new XAttribute("set", set.Identifier), + new XAttribute("count", count)); + finishedEventsElement.Add(element); + } + newElement.Add(finishedEventsElement); + } } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 3766dd230..1a1210460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -95,7 +95,7 @@ namespace Barotrauma set; } - [Serialize("20,40,50", IsPropertySaveable.Yes), Editable()] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] public Color BackgroundTextureColor { get; @@ -176,7 +176,7 @@ namespace Barotrauma set; } - [Serialize(false, IsPropertySaveable.Yes, ""), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable] public bool NoLevelGeometry { get; @@ -218,21 +218,21 @@ namespace Barotrauma set { height = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Minimum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMin { get { return initialDepthMin; } set { initialDepthMin = Math.Max(value, 0); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Maximum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMax { get { return initialDepthMax; } set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + [Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] public int MinTunnelRadius { get; @@ -240,7 +240,7 @@ namespace Barotrauma } - [Serialize("0,1", IsPropertySaveable.Yes), Editable] + [Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable] public Point SideTunnelCount { get; @@ -248,14 +248,14 @@ namespace Barotrauma } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SideTunnelVariance { get; set; } - [Serialize("2000,6000", IsPropertySaveable.Yes), Editable] + [Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable] public Point MinSideTunnelRadius { get; @@ -336,7 +336,7 @@ namespace Barotrauma } } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float MainPathVariance { get; @@ -385,28 +385,28 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float CaveResourceSpawnChance { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of floating, destructible ice chunks in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of islands (static wall chunks along the main path) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100)] public int IslandCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of ice spires in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int IceSpireCount { get; set; } - [Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes, description: "Number of abyss islands in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { get; @@ -427,7 +427,7 @@ namespace Barotrauma set; } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable()] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "The probability of an abyss island having a cave. There is always a cave in at least one of the islands regardless of this setting."), Editable()] public float AbyssIslandCaveProbability { get; @@ -532,7 +532,7 @@ namespace Barotrauma public float WreckFloodingHullMaxWaterPercentage { get; set; } #endregion - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Should a beacon station always spawn in this type of level?")] public string ForceBeaconStation { get; set; } [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] @@ -571,21 +571,21 @@ namespace Barotrauma private set; } - [Serialize("0,0", IsPropertySaveable.Yes), Editable] + [Serialize("0,0", IsPropertySaveable.Yes, description: "Interval of lightning-like flashes of light in the level."), Editable] public Vector2 FlashInterval { get; set; } - [Serialize("0,0,0,0", IsPropertySaveable.Yes), Editable] + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Color of lightning-like flashes of light in the level."), Editable] public Color FlashColor { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the \"ambient noise\" of the biome play in this level if it's an outpost level."), Editable] public bool PlayNoiseLoopInOutpostLevel { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 213fad7bf..60d7ce783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -129,7 +129,7 @@ namespace Barotrauma private set; } - [Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable] + [Serialize("0.0,1.0", IsPropertySaveable.Yes, description: "The sprite depth of the object (min, max). Values of 0 or less make the object render in front of walls, values larger than 0 make it render behind walls with a parallax effect."), Editable] public Vector2 DepthRange { get; @@ -273,14 +273,14 @@ namespace Barotrauma private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the object disappear if the object is destroyed? Only relevant if TakeLevelWallDamage is true."), Editable] public bool HideWhenBroken { get; private set; } - [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Amount of health the object has. Only relevant if TakeLevelWallDamage is true."), Editable] public float Health { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index c922a86c9..dbc7050c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; @@ -658,6 +659,10 @@ namespace Barotrauma { effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); } + else if (triggerer is Submarine sub) + { + effect.Apply(effect.type, deltaTime, sub, Array.Empty(), position); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 6ae55544f..296df273b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -95,6 +95,8 @@ namespace Barotrauma public Reputation Reputation => Faction?.Reputation; + public bool IsFactionHostile => Faction?.Reputation.NormalizedValue < Reputation.HostileThreshold; + public int TurnsInRadiation { get; set; } #region Store @@ -177,29 +179,30 @@ namespace Barotrauma } } + public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo) + { + int quantity = PriceInfo.DefaultAmount; + if (priceInfo.MaxAvailableAmount > 0) + { + quantity = + priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount ? + Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1) : + priceInfo.MaxAvailableAmount; + } + else if (priceInfo.MinAvailableAmount > 0) + { + quantity = priceInfo.MinAvailableAmount; + } + return new PurchasedItem(itemPrefab, quantity, buyer: null); + } + public List CreateStock() { var stock = new List(); foreach (var prefab in ItemPrefab.Prefabs) { if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; } - int quantity = PriceInfo.DefaultAmount; - if (priceInfo.MaxAvailableAmount > 0) - { - if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) - { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); - } - else - { - quantity = priceInfo.MaxAvailableAmount; - } - } - else if (priceInfo.MinAvailableAmount > 0) - { - quantity = priceInfo.MinAvailableAmount; - } - stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); + stock.Add(CreateInitialStockItem(prefab, priceInfo)); } return stock; } @@ -304,6 +307,7 @@ namespace Barotrauma if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); + price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all"))); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); @@ -811,30 +815,36 @@ namespace Barotrauma return null; } AddMission(mission); + DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true); return mission; } return null; } - public Mission UnlockMissionByTag(Identifier tag) + public Mission UnlockMissionByTag(Identifier tag, Random random = null) { if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag)); - if (!matchingMissions.Any()) + if (matchingMissions.None()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); } else { - var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m)); + var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m)); if (unusedMissions.Any()) { var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this))); - if (!suitableMissions.Any()) + if (suitableMissions.None()) { suitableMissions = unusedMissions; } - MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced); + + MissionPrefab missionPrefab = + random != null ? + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, Rand.RandSync.Unsynced); + var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) @@ -842,6 +852,7 @@ namespace Barotrauma return null; } AddMission(mission); + DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true); return mission; } else @@ -876,7 +887,7 @@ namespace Barotrauma } var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); - if (!suitableConnections.Any()) + if (suitableConnections.None()) { suitableConnections = Connections.ToList(); } @@ -1270,25 +1281,31 @@ namespace Barotrauma } var stock = new List(store.Stock); var stockToRemove = new List(); - foreach (var item in stock) + + foreach (var itemPrefab in ItemPrefab.Prefabs) { - if (item.ItemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) + var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab); + if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) { - item.Quantity += 1; - if (priceInfo.MaxAvailableAmount > 0) + if (existingStock == null) { - item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + //can be bought from the location, but not in stock - some new item added by an update or mod? + stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo)); } else { - item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + existingStock.Quantity = + Math.Min( + existingStock.Quantity + 1, + priceInfo.MaxAvailableAmount > 0 ? priceInfo.MaxAvailableAmount : CargoManager.MaxQuantity); } } - else + else if (existingStock != null) { - stockToRemove.Add(item); + stockToRemove.Add(existingStock); } } + stockToRemove.ForEach(i => stock.Remove(i)); store.Stock.Clear(); store.Stock.AddRange(stock); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 494653ba2..e361bd38b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -1456,7 +1456,13 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "location": - Location location = Locations[subElement.GetAttributeInt("i", 0)]; + int locationIndex = subElement.GetAttributeInt("i", -1); + if (locationIndex < 0 || locationIndex >= Locations.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})"); + continue; + } + Location location = Locations[locationIndex]; location.ProximityTimer.Clear(); for (int i = 0; i < location.Type.CanChangeTo.Count; i++) { @@ -1502,9 +1508,17 @@ namespace Barotrauma break; case "connection": - int connectionIndex = subElement.GetAttributeInt("i", 0); + //the index wasn't saved previously, skip if that's the case + if (subElement.Attribute("i") == null) { continue; } + + int connectionIndex = subElement.GetAttributeInt("i", -1); + if (connectionIndex < 0 || connectionIndex >= Connections.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})"); + continue; + } Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false); - Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); + Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); @@ -1635,6 +1649,7 @@ namespace Barotrauma new XAttribute("locked", connection.Locked), new XAttribute("difficulty", connection.Difficulty), new XAttribute("biome", connection.Biome.Identifier), + new XAttribute("i", i), new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1]))); connection.LevelData.Save(connectionElement); mapElement.Add(connectionElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 4a3f82436..35a758882 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -24,7 +24,7 @@ namespace Barotrauma } - [Serialize(-1, IsPropertySaveable.Yes), Editable(MinValueInt = -1, MaxValueInt = 10)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)] public int ForceToEndLocationIndex { get; @@ -32,7 +32,7 @@ namespace Barotrauma } - [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] + [Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { get; @@ -46,70 +46,70 @@ namespace Barotrauma set; } - [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(200.0f, IsPropertySaveable.Yes, description: "Minimum length of the hallways between modules. If 0, the generator will place the modules directly against each other assuming it can be done without making any modules overlap."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable] public bool AlwaysDestructible { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be rewireable, regardless if rewiring is allowed by the server?"), Editable] public bool AlwaysRewireable { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should stealing from this outpost be always allowed?"), Editable] public bool AllowStealing { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the crew spawn inside the outpost (if not, they'll spawn in the submarine)."), Editable] public bool SpawnCrewInsideOutpost { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should doors at the edges of an outpost module that didn't get connected to another module be locked?"), Editable] public bool LockUnusedDoors { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should gaps at the edges of an outpost module that didn't get connected to another module be removed?"), Editable] public bool RemoveUnusedGaps { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the whole outpost render behind submarines? Only set this to true if the submarine is intended to go inside the outpost."), Editable] public bool DrawBehindSubs { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MaxWaterPercentage { get; @@ -122,7 +122,7 @@ namespace Barotrauma set; } - [Serialize("", IsPropertySaveable.Yes), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable] public string ReplaceInRadiation { get; set; } public ContentPath OutpostFilePath { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 6b95736f8..c7d5968ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -306,7 +306,7 @@ namespace Barotrauma } idOffset = moduleEntities.Max(e => e.ID) + 1; - var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); + var wallEntities = moduleEntities.Where(e => e is Structure s && s.HasBody).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); // Tell the hulls what tags the module has, used to spawn NPCs on specific rooms @@ -1440,27 +1440,7 @@ namespace Barotrauma private static void EnableFactionSpecificEntities(Submarine sub, Location location) { - foreach (MapEntity me in MapEntity.mapEntityList) - { - if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; } - - var layerAsIdentifier = me.Layer.ToIdentifier(); - if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) - { - me.HiddenInGame = - location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; -#if CLIENT - //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that - if (me.HiddenInGame && me is Item item) - { - foreach (var lightComponent in item.GetComponents()) - { - lightComponent.Light.Enabled = false; - } - } -#endif - } - } + sub.EnableFactionSpecificEntities(location?.Faction?.Prefab.Identifier ?? Identifier.Empty); } private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 794a9671b..9a9405020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -93,13 +93,11 @@ namespace Barotrauma { moduleFlags.Add("hallwayhorizontal".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (newFlags.Contains("hallwayvertical".ToIdentifier())) { moduleFlags.Add("hallwayvertical".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (!newFlags.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8cd78e3f1..d77a65b7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -939,7 +939,7 @@ namespace Barotrauma Rand.Range(worldRect.X, worldRect.Right + 1), Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); + var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } @@ -1085,9 +1085,9 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true) { - if (Submarine != null && Submarine.GodMode || Indestructible) { return; } + if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; } if (!Prefab.Body) { return; } if (!MathUtils.IsValid(damage)) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 97a7eec42..941ddab07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.IO; using System.Collections.Immutable; +using System.ComponentModel; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -44,30 +45,26 @@ namespace Barotrauma public override ImmutableHashSet Aliases { get; } - //does the structure have a physics body - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Does the structure have a physics body?")] public bool Body { get; private set; } - //rotation of the physics body in degrees - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the physics body in degrees.")] public float BodyRotation { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Width of the physics body in pixels.")] public float BodyWidth { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Height of the physics body in pixels.")] public float BodyHeight { get; private set; } //in display units - [Serialize("0.0,0.0", IsPropertySaveable.No)] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "Offset of the physics body from the center of the structure in pixels.")] public Vector2 BodyOffset { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Is the structure a platform (i.e. a \"floor\" the players can pass through)? Only relevant if the structure has a physics body.")] public bool Platform { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can items like signal components be attached on this structure? Should be enabled on structures like decorative background walls.")] public bool AllowAttachItems { get; private set; } [Serialize(0.0f, IsPropertySaveable.No)] @@ -81,27 +78,30 @@ namespace Barotrauma private set { health = Math.Max(value, MinHealth); } } - [Serialize(true, IsPropertySaveable.No)] + [Serialize(true, IsPropertySaveable.No, description: "Should the structure be indestructible when used in an outpost?")] public bool IndestructibleInOutposts { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Should the structure cast shadows and obstruct visibility when LOS is enabled?")] public bool CastShadow { get; private set; } - [Serialize(Direction.None, IsPropertySaveable.No)] + [Serialize(Direction.None, IsPropertySaveable.No, description: "Makes the structure function as a staircase.")] public Direction StairDirection { get; private set; } - [Serialize(45.0f, IsPropertySaveable.No)] + [Serialize(45.0f, IsPropertySaveable.No, description: "Angle of the stairs in degrees. Only relevant if StairDirection is something else than None.")] public float StairAngle { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, monsters will not be able to target this structure.")] public bool NoAITarget { get; private set; } - [Serialize("0,0", IsPropertySaveable.Yes)] + [Serialize("0,0", IsPropertySaveable.Yes, description: "Size of the structure in pixels. If not set, the size is determined, based on the attributes width and height, and if those aren't defined either, based on the size of the structure's sprite.")] public Vector2 Size { get; private set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the sound that plays when something damages the wall.")] public string DamageSound { get; private set; } + [Serialize("shrapnel", IsPropertySaveable.Yes, description: "Identifier of the particles emitted when something damages the wall.")] + public string DamageParticle { get; private set; } + protected Vector2 textureScale = Vector2.One; [Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)] public Vector2 TextureScale diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 85ddf7425..0ec801d02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1,16 +1,13 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.ComponentModel; -using Barotrauma.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using System.Xml.Linq; using Voronoi2; @@ -1044,6 +1041,30 @@ namespace Barotrauma #endif } + public void EnableFactionSpecificEntities(Identifier factionIdentifier) + { + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } + + var layerAsIdentifier = me.Layer.ToIdentifier(); + if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) + { + me.HiddenInGame = factionIdentifier != layerAsIdentifier; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } + } + } + public void Update(float deltaTime) { if (Info.IsWreck) @@ -1098,7 +1119,7 @@ namespace Barotrauma public void ApplyForce(Vector2 force) { - if (subBody != null) subBody.ApplyForce(force); + if (subBody != null) { subBody.ApplyForce(force); } } public void EnableMaintainPosition() @@ -1690,9 +1711,30 @@ namespace Barotrauma } } + Dictionary savedEntities = new Dictionary(); foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { if (!e.ShouldBeSaved) { continue; } + + if (e.Removed) + { + GameAnalyticsManager.AddErrorEventOnce( + "Submarine.SaveToXElement:Removed" + e.Name, + GameAnalyticsManager.ErrorSeverity.Error, + $"Attempted to save a removed entity (\"{e.Name}\"). Duplicate ID: {savedEntities.ContainsKey(e.ID)}"); + DebugConsole.ThrowError($"Error while saving the submarine. Attempted to save a removed entity (\"{e.Name} ({e.ID})\"). The entity will not be saved to avoid corrupting the submarine file."); + continue; + } + if (savedEntities.TryGetValue(e.ID, out MapEntity duplicateEntity)) + { + GameAnalyticsManager.AddErrorEventOnce( + "Submarine.SaveToXElement:DuplicateId" + e.Name, + GameAnalyticsManager.ErrorSeverity.Error, + $"Attempted to save an entity with a duplicate ID ({e.Name}, {duplicateEntity.Name})."); + DebugConsole.ThrowError($"Error while saving the submarine. The entity \"{e.Name}\" has the same ID as \"{duplicateEntity.Name}\" ({e.ID}). The entity will not be saved to avoid corrupting the submarine file."); + continue; + } + if (e is Item item) { if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } @@ -1712,6 +1754,7 @@ namespace Barotrauma } e.Save(element); + savedEntities.Add(e.ID, e); } Info.CheckSubsLeftBehind(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index e498b3aa0..064687270 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -39,6 +39,8 @@ namespace Barotrauma private const float MaxCollisionImpact = 5.0f; private const float Friction = 0.2f, Restitution = 0.0f; + private readonly List levelContacts = new List(); + public List HullVertices { get; @@ -56,6 +58,9 @@ namespace Barotrauma private readonly Queue impactQueue = new Queue(); + private float forceUpwardsTimer; + private const float ForceUpwardsDelay = 30.0f; + struct Impact { public Fixture Target; @@ -199,6 +204,7 @@ namespace Barotrauma if (item.Submarine != submarine) { continue; } Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); + if (sub.FlippedX) { simPos.X = -simPos.X; } if (item.GetComponent() is Door door) { door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith); @@ -215,11 +221,6 @@ namespace Barotrauma float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); - if (sub.FlippedX) - { - simPos.X = -simPos.X; - } - if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith)); @@ -393,6 +394,33 @@ namespace Barotrauma //------------------------- + //if heading up and there's another sub on top of us, gradually force it upwards + //(i.e. apply "artificial buoyancy" to it) to prevent us from getting pinned under it + //only applies to enemy subs with no enemies inside them (like destroyed pirate subs) + if (totalForce.Y > 0) + { + ContactEdge contactEdge = Body?.FarseerBody?.ContactList; + while (contactEdge?.Contact != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Other.UserData is Submarine otherSubmarine && + otherSubmarine.TeamID != Submarine.TeamID && + contactEdge.Contact.IsTouching) + { + contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2 points); + if (points[0].Y > Body.SimPosition.Y && + !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID)) + { + otherSubmarine.GetConnectedSubs().ForEach(s => s.SubBody.forceUpwardsTimer += deltaTime); + break; + } + } + contactEdge = contactEdge.Next; + } + } + + //------------------------- + if (Body.LinearVelocity.LengthSquared() > 0.0001f) { //TODO: sync current drag with clients? @@ -416,7 +444,32 @@ namespace Barotrauma ApplyForce(totalForce); + if (Velocity.LengthSquared() < 0.01f) + { + levelContacts.Clear(); + levelContacts.AddRange(GetLevelContacts(Body)); + for (int i = 0; i < levelContacts.Count; i++) + { + for (int j = i + 1; j < levelContacts.Count; j++) + { + levelContacts[i].GetWorldManifold(out Vector2 normal1, out _); + levelContacts[j].GetWorldManifold(out Vector2 normal2, out _); + + //normals pointing in different directions = sub lodged between two walls + if (Vector2.Dot(normal1, normal2) < 0) + { + //apply an extra force to hopefully dislodge the sub + ApplyForce(totalForce * 100.0f); + i = levelContacts.Count; + break; + } + } + } + } + UpdateDepthDamage(deltaTime); + + forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay); } partial void ClientUpdatePosition(float deltaTime); @@ -483,9 +536,18 @@ namespace Barotrauma float buoyancy = NeutralBallastPercentage - waterPercentage; if (buoyancy > 0.0f) + { buoyancy *= 2.0f; + } else + { buoyancy = Math.Max(buoyancy, -0.5f); + } + + if (forceUpwardsTimer > 0.0f) + { + buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); + } return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); } @@ -630,7 +692,7 @@ namespace Barotrauma } //if all the bodies of a wall have been disabled, we don't need to care about gaps (can always pass through) - if (!(contact.FixtureA.UserData is Structure wall) || !wall.AllSectionBodiesDisabled()) + if (contact.FixtureA.UserData is not Structure wall || !wall.AllSectionBodiesDisabled()) { var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); @@ -682,20 +744,10 @@ namespace Barotrauma } //find all contacts between the limb and level walls - List levelContacts = new List(); - ContactEdge contactEdge = limb.body.FarseerBody.ContactList; - while (contactEdge?.Contact != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Contact.IsTouching && - contactEdge.Other?.UserData is VoronoiCell) - { - levelContacts.Add(contactEdge.Contact); - } - contactEdge = contactEdge.Next; - } + IEnumerable levelContacts = GetLevelContacts(limb.body); + int levelContactCount = levelContacts.Count(); - if (levelContacts.Count == 0) { return; } + if (levelContactCount == 0) { return; } //if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it //not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete), @@ -718,9 +770,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the limb - ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false); + ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal); if (contactDot > 0.001f) @@ -759,6 +811,21 @@ namespace Barotrauma } } + private static IEnumerable GetLevelContacts(PhysicsBody body) + { + ContactEdge contactEdge = body.FarseerBody.ContactList; + while (contactEdge?.Contact != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching && + contactEdge.Other?.UserData is VoronoiCell) + { + yield return contactEdge.Contact; + } + contactEdge = contactEdge.Next; + } + } + private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10) @@ -827,21 +894,9 @@ namespace Barotrauma } //find all contacts between this sub and level walls - List levelContacts = new List(); - ContactEdge contactEdge = Body?.FarseerBody?.ContactList; - while (contactEdge?.Next != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Other.UserData is VoronoiCell && - contactEdge.Contact.IsTouching) - { - levelContacts.Add(contactEdge.Contact); - } - - contactEdge = contactEdge.Next; - } - - if (levelContacts.Count == 0) { return; } + IEnumerable levelContacts = GetLevelContacts(Body); + int levelContactCount = levelContacts.Count(); + if (levelContactCount == 0) { return; } //if this sub is in contact with the level, apply artifical impacts //to both subs to prevent the other sub from bouncing on top of this one @@ -852,8 +907,7 @@ namespace Barotrauma levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2 temp); //if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal - VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ? - ((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData); + VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell; var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center; if (Vector2.Dot(contactNormal, cellDiff) < 0) @@ -864,9 +918,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the level - ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos); + ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; //apply an impact to the other sub float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal); diff --git a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs index f28300c70..7398a7f21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs @@ -94,6 +94,7 @@ namespace Barotrauma { List bytes = new List(); byte currentByte; + do { if (inc.BitPosition >= inc.LengthBits) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index d453464f8..e88558001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -87,6 +87,7 @@ namespace Barotrauma.Networking if (character != null) { HasSpawned = true; + UsingFreeCam = false; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); @@ -100,6 +101,11 @@ namespace Barotrauma.Networking } } + /// + /// Is the client using the 'freecam' console command? + /// + public bool UsingFreeCam; + public UInt16 CharacterID; private Vector2 spectatePos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 8bde51456..b17701a49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -28,28 +28,30 @@ namespace Barotrauma.Networking ManageMap = 0x8000, ManageHires = 0x10000, ManageBotTalents = 0x20000, - All = 0x3FFFF + SpamImmunity = 0x40000, + All = 0x7FFFF } class PermissionPreset { public static readonly List List = new List(); - - public readonly LocalizedString Name; + + public readonly Identifier Identifier; + public readonly LocalizedString DisplayName; public readonly LocalizedString Description; public readonly ClientPermissions Permissions; public readonly HashSet PermittedCommands; public PermissionPreset(XElement element) { - string name = element.GetAttributeString("name", ""); - Name = TextManager.Get("permissionpresetname." + name).Fallback(name); - Description = TextManager.Get("permissionpresetdescription." + name) .Fallback(element.GetAttributeString("description", "")); + Identifier = element.GetAttributeIdentifier("name", Identifier.Empty); + DisplayName = TextManager.Get("permissionpresetname." + Identifier).Fallback(Identifier.ToString()); + Description = TextManager.Get("permissionpresetdescription." + Identifier) .Fallback(element.GetAttributeString("description", "")); string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) { - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + permissionsStr + " is not a valid permission!"); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + permissionsStr + " is not a valid permission!"); } PermittedCommands = new HashSet(); @@ -64,7 +66,7 @@ namespace Barotrauma.Networking if (command == null) { #if SERVER - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + commandName + "\" is not a valid console command."); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + commandName + "\" is not a valid console command."); #endif continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index fcc0bed07..6f5bf9235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -39,6 +39,7 @@ namespace Barotrauma.Networking ServerMessage, ConsoleUsage, Money, + DoSProtection, Karma, Talent, Error, @@ -55,6 +56,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, { MessageType.Money, Color.Green }, + { MessageType.DoSProtection, Color.OrangeRed }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, { MessageType.Error, Color.Red } @@ -71,6 +73,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, { MessageType.Money, "Money" }, + { MessageType.DoSProtection, "DoSProtection" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, { MessageType.Error, "Error" } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index cdeaa24de..7448dcd5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -42,6 +42,11 @@ namespace Barotrauma.Networking partial class ServerSettings : ISerializableEntity { + public const int PacketLimitMin = 1200, + PacketLimitWarning = 2400, + PacketLimitDefault = 3000, + PacketLimitMax = 10000; + public const string SettingsFile = "serversettings.xml"; [Flags] @@ -688,6 +693,20 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool EnableDoSProtection + { + get; + private set; + } + + [Serialize(PacketLimitDefault, IsPropertySaveable.Yes)] + public int MaxPacketAmount + { + get; + private set; + } + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSubmarine { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 60b0aca6d..b8e5e84f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -39,16 +39,10 @@ namespace Barotrauma public DelayedEffect(ContentXElement element, string parentDebugName) : base(element, parentDebugName) { - string delayTypeStr = element.GetAttributeString("delaytype", "timer"); - if (!Enum.TryParse(typeof(DelayTypes), delayTypeStr, ignoreCase: true, out var delayType)) + DelayTypes delayTypeAttr = element.GetAttributeEnum("delaytype", DelayTypes.Timer); + if (delayTypeAttr is DelayTypes.Timer) { - DebugConsole.ThrowError("Invalid delay type \"" + delayTypeStr + "\" in StatusEffect (" + parentDebugName + ")"); - } - switch (delayType) - { - case DelayTypes.Timer: - delay = element.GetAttributeFloat("delay", 1.0f); - break; + delay = element.GetAttributeFloat("delay", 1.0f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index b75364536..5ccdfc56c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -899,7 +899,8 @@ namespace Barotrauma if (HasTargetType(TargetType.NearbyItems)) { //optimization for powered components that can be easily fetched from Powered.PoweredList - if (TargetIdentifiers.Count == 1 && + if (TargetIdentifiers != null && + TargetIdentifiers.Count == 1 && (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent"))) { foreach (Powered powered in Powered.PoweredList) @@ -2191,7 +2192,7 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { - afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.BuffItemApplyingMultiplier); } else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned")) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 0cd9799f9..08658ad68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Steam { "language", 5 } }; - public static bool IsInitialized { get; private set; } + public static bool IsInitialized => IsInitializedProjectSpecific; private static readonly List popularTags = new List(); public static IEnumerable PopularTags @@ -189,7 +189,6 @@ namespace Barotrauma.Steam if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } - IsInitialized = false; } public static IEnumerable ParseWorkshopIds(string workshopIdData) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index e7071ddc0..5b6f6f15f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -194,7 +194,7 @@ namespace Barotrauma.Steam { try { - System.IO.Directory.Delete(item.Directory, recursive: true); + System.IO.Directory.Delete(item.Directory ?? "", recursive: true); } catch { @@ -312,7 +312,7 @@ namespace Barotrauma.Steam public static bool IsItemDirectoryUpToDate(in Steamworks.Ugc.Item item) { - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; return Directory.Exists(itemDirectory) && File.GetLastWriteTime(itemDirectory).ToUniversalTime() >= item.LatestUpdateTime; } @@ -432,9 +432,9 @@ namespace Barotrauma.Steam if (!(itemNullable is { } item)) { return; } await Task.Yield(); - string itemTitle = item.Title.Trim(); + string itemTitle = item.Title?.Trim() ?? ""; UInt64 itemId = item.Id; - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; DateTime updateTime = item.LatestUpdateTime; if (!CanBeInstalled(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index ee733cbe1..6fae4f6cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -13,7 +13,7 @@ namespace Barotrauma { private const float UpdateInterval = 1.0f; - private static HashSet unlockedAchievements = new HashSet(); + private static readonly HashSet unlockedAchievements = new HashSet(); public static bool CheatsEnabled = false; @@ -442,7 +442,7 @@ namespace Barotrauma var charactersInSub = Character.CharacterList.FindAll(c => !c.IsDead && c.TeamID != CharacterTeamType.FriendlyNPC && - !(c.AIController is EnemyAIController) && + c.AIController is not EnemyAIController && (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); if (charactersInSub.Count == 1) @@ -524,7 +524,10 @@ namespace Barotrauma public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } - + if (Screen.Selected is { IsEditor: true }) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TestGameMode) { return; } +#endif #if SERVER if (unlockClients && GameMain.Server != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 08254fa0d..9cbc56614 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -394,7 +394,6 @@ namespace Barotrauma return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); } - public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { List objectList = objects.ToList(); @@ -409,7 +408,7 @@ namespace Barotrauma public static T SelectWeightedRandom(IList objects, IList weights, Random random) { - if (objects.Count == 0) return default(T); + if (objects.Count == 0) { return default(T); } if (objects.Count != weights.Count) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 232affe74..3eb16ac17 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,199 @@ +--------------------------------------------------------------------------------------------------------- +v1.0.13.1 +--------------------------------------------------------------------------------------------------------- + +- Updated localizations. +- Fixes to Japanese and Russian translations. +- Fixed Azimuth not going up in Silent Running mode. + +--------------------------------------------------------------------------------------------------------- +v1.0.13.0 +--------------------------------------------------------------------------------------------------------- + +Misc changes and improvements: +- NPCs who offer services don't get turned hostile regardless of your reputation. If you've got money, they'll be happy to serve! +- Attacking outpost NPCs can't decrease your reputation by more than 20 points per round. Works as a safeguard against enormous reputation losses e.g. in the case of a trigger-happy griefer or a nuclear mishap. +- Destroying outpost walls can't decrease your reputation by more than 10 points per round. +- Outpost NPCs don't allow players to grab them for longer than 10 seconds to prevent being able to drag them around the outpost. +- Minor visual improvements to biomes: biome-specific outpost levels (instead of all outpost levels looking like Cold Caverns), more level objects in Hydrothermal Wastes and the Great Sea. +- Spawn abyss and combat suits in enemy subs and wrecks instead of normal ones in later biomes. +- Fixed minerals still sometimes spawning on the wrong side of cave walls (when the other side of the wall is outside the boundaries of the level). +- Fixed pressure stabilizer only affecting the player for 100 seconds, instead of the intended 1000 seconds (16 mins). +- Fixed clients not getting assigned the "None" permission preset when using a language other than English (meaning it wasn't possible to customize what permissions clients have by default). +- Fixed enabling cheats not actually disabling Steam achievements for the rest of the campaign (it was possible to re-enable unlocking achievements by saving and reloading). +- Fixed Tormsdale mission not completing unless you bring the item to the sub. Now retrieving the item counts as "bringing it to the sub" in friendly outposts. +- Optimized/simplified exosuit and FB3000 status effects. +- Made huskified humans' items move to a duffel bag on death. +- Increased the priority of turret lights to prevent them from getting hidden when using a low light limit. +- Reduced the minimum mass required for a character to be visible with thermal goggles, always show at least the main limb regardless of the mass. Fixes thresher hatchlings being invisible to the goggles. +- Alien power cells can be deconstructed. + +Bots: +- Fixed bots targeting crawler eggs when they are inside an inventory (also in the Broodmother's inventory). +- Fixed bots being allowed to shoot items from very long distances. +- Fixed bots wasting ammunition on crawler eggs that are not dangerously close to the sub. +- Fixed bots using an unintentionally long delay before shooting. +- Fixed bots targeting (but not shooting) enemies inside abandoned outposts. +- Adjusted the delays and the targeting ranges. Remove the "grace" distance modifier. Improves the bots' general usage of turrets. +- Fixed "fight intruders" order causing bots to attack enemies in abandoned outposts again. +- Bots are allowed to use meds from an unconscious patient's inventory. +- Fixed bots falling off the ledge in DockingModule_02_Colony (again). +- Fixed bots not prioritizing the leaks as they should when multiple bots are fixing leaks simultaneously. +- Fixed bots not preferring the items that lie on the ground. +- Fixed bots often not being able to reach small items like battery cells or rifle rounds that lie on the ground. +- Fixed bots sometimes holding the flashlight in their mouth. +- Fixed bots acting weird when picking up items and having their inventory full. +- Fixed bots not being able to find an alternative container for the items they are cleaning up, when the path to the preferred container is blocked. Didn't affect bots getting a specific item, like a diving suit when they need it. +- Bots are no longer allowed to wear the items (other than in hands) when they are cleaning them up. +- Improved bots' abilities to find items. They should be much quicker at it than previously. +- Possibly fixed bots sometimes ignoring targets that they shouldn't ignore. +- Fixed bots not being able to change the oxygen tanks when they are in a safe room with no enemies. +- Fixed bots using both diving mask and the suit simultaneously. +- Fixed bots holding to old paths that shouldn't be valid anymore (= not complying when you order them to follow outside of the submarine). +- Fixed bots not being able to use pathing when the player has just controlled them, leaving them in a ruin, wreck, or a beacon. Only happened when the bot hadn't been AI controlled before during the round. +- Fixed bots using the waypoints not linked to any sub when they should use the waypoints linked to a sub and vice versa. +- Fixed bots trying to use the gaps when they could just use a path to get to the target. +- Fixed missing links between doors and waypoints in Alien_Entrance3. +- Fixed bots sometimes not being able to release from the ladders when they were trying to exit a wreck or beacon to get back to their own submarine. +- Fixed bots sometimes still idling on ladders, which they shouldn't do (There are some unaddressed cases where it might seem like they'd do this, but actually don't. They just can't reach the buttons linked to the door.) +- Fixed bots sometimes failing to climb up the ladder all the way up and falling down just before reaching next floor. Happened only in the idle state. +- Fixed bots sometimes getting stuck on the long ladder in EngineeringModule_02_Colony. +- Fixed waypoints on ladders leading to a hatch on many subs (using old waypoints), which caused the bots to fail to reach the hatch while trying to fix it. +- Fixed bots not being able to operate properly in high pressure levels when they don't have a suit that protects from the pressure (e.g. a regular diving suit later in the campaign). +- Bots now prioritize suits that give a better pressure protection (there's a separate attribute for it in the item definition: called "botpriority", which can and should be taken into account with the modified items). +- Fixed bots not healing theirselves while swimming outside of the submarine (they only do that when the wounds are relatively severe). +- Fixed bots treating theirselves when there's a medic onboard, which is currently unable to treat them (if the medic is able to treat the bot, they should not try to heal theirselves). +- Fixed bots ignoring targets that are currently fixing leaks (which was intentional, but seems to have been a bad decision). +- Added new dialogue for the bots about the lethal pressure levels and insufficient protection for it. +- Fixed some minor buts regarding the dialogue. +- Fixed bots not being able to use harpoon coil rifles properly. +- Fixed bots accepting random items as weapons when they can't find any weapon. Note that many tools, like welding tools and wrenches, can intentionally be used as weapons. +- Fixed prepare for expedition order and another order, like clean items, possibly resulting in looping behavior where the bot can't decide which one to follow. +- Adjustments to how the bots prioritize combat targets under fight intruders objective. +- Fixed bots not reacting when they are non-intentionally damaged by friendly NPCs (e.g. security smashing a player character). + +Multiplayer fixes: +- Changes to make starting a round more robust: fixes various equality check errors ("submarine/mission doesn't match") if starting a multiplayer round takes a long time. +- Fixed dedicated servers' content package info getting truncated to 255 bytes, causing the content package list to just display "unknown" if the server has lots of mods enabled. +- Fixed "input contains duplicate packages" error when trying to join a server that has certain types of mods enabled (more specifically, mods that only contain client-side content, identical content or files of the type "Other", which could result in the MD5 hashes of the mods to be identical). +- Fixed projectile spread not being as random as it should be in multiplayer (successive hitscan rounds usually launched in the same direction). +- Fixed spectators not hearing others if their character is dead before the character despawns. +- Campaign rounds aren't forced to end if there's only one client on the server using freecam. +- Fixed crashing when a traitor missions starts when the host isn't controlling a character. +- Fixed sonar beacon tickbox sometimes flickering on and off in multiplayer. +- Fixed clients not seeing wall damage in outposts if the server has made outpost walls damageable, and the client doesn't have permissions to manage server settings. +- Fixed arc emitter briefly stunning the user client-side. +- Unconscious players can't end the round. + +Countermeasures against multiplayer exploits: +- Fixed an exploit that allowed you to equip 2-handed weapons in only one hand. +- Added protection against deliberately lagging the server. + - Added an option to enable "DoS Protection" in the server settings under "Anti-Griefing". + - Enabled by default. + - When enabled, the server will automatically kick players who are causing the server to perform poorly. + - Added a new "Max Packet Auto-Kick" in the server settings under "Anti-Griefing". + - Enabled and set to 3000 by default. + - Can be disabled by setting the limit to 1200 or below. + - When enabled, the server will automatically kick players who are sending a certain amount of network packets in a minute. +- Added "Spam Immunity" server permission. + - Gives immunity to getting kicked from DoS protection, chat spam and from sending too many packets. +- Added a rate limit to console commands in multiplayer. +- Added a rate limit to creating new characters in multiplayer. + +Balance: +- Buffed Harpoon Coil Rifle a bit (shorter charge time). +- Reduced Focused Flak Shell penetration slightly. + +Talents: +- Engine Engineer, Helmsman and Affiliation talents no longer stack (allowed gaining insane engine speed boosts and bonuses by having large numbers of crew with the same talent). +- Fixed makeshift shelves no longer being placeable in pre-1.0 saves. +- Fixed some incorrect talent icons. +- Fixed Networking talent only giving a discount for faction-specific items. +- Fixed Machine Maniac requiring 5 repaired items instead 3 like the description says. +- Gene harvester doesn't spawn genetic materials on pets. +- Dying due to a disconnect doesn't trigger talents (like "Revenge Squad") or get recorded as a kill. + +Submarines: +- Reworked Typhon (kudos to uberpendragon): replaced legacy items and structures, got rid of double walls, layout adjustments, visual improvements, improvements to the power grid and many smaller changes. +- Fixed waypoint in Herja's airlock not being linked to the door. +- Fixed fabricators not being linked to the cabinet next to them in Azimuth, Berilia, Kastrull, Orca2, Typhon, Typhon2, Winterhalter. +- Fixed some unhulled spaces inside Kastrull, Azimuth and Berilia. +- Fixed improperly wired smoke detectors in Typhon 2. +- Replaced the wall between gunnery and engineering (that every sane person cuts a hole in) with a door in Kastrull. +- Fixed medic not being given proper ID card tags in R-29, preventing them from accessing the toxin cabinet. +- Fixed Venture's exterior airlock door being repairable with a welding tool instead of a wrench. +- Fixed water detector in Dugong's oxygen generator room not being connected to the flood alarm circuit. +- Added "Silent Running" to Azimuth, as well as 1 diving suit and exterior cameras to make it more worthy of tier 2 (despite a lack of guns) + +Fixes: +- Fixed all shadow-casting lights going through the outpost walls when docked to one. +- Oxygen generator sprite and animation fixes. +- Fixed escorted characters being hostile to you if they belong to a hostile faction. +- Safeguard against getting pinned under flooded enemy subs. The sub can now be pushed off as long as the enemies inside it are dead/incapacitated. +- Fixed enemy subs not affecting monster spawns (meaning monsters could spawn very close to enemy subs, and fighting an enemy sub didn't decrease the probability of monster spawns as it should've). +- Fixed submarines sometimes getting stuck when trying to squeeze through a tight passage in the level. +- Fixed pets being considered hostile in hostile outposts (causing AI crew to report them as intruders). +- Fixed monsters sometimes spawning inside walls during nest missions. +- Fixed paths between biomes sometimes being unlocked from the start on certain map seeds. +- Fixed launching an alien turret (or any modded turret that spawns it's own ammo inside itself) causing a crash. +- Fixed crashing when you exit Steam while the Workshop menu is open. +- Fixed some of the fonts not working properly in Japanese (but using roughly similar Chinese symbols instead). +- Fixed campaign's end boss moving away from you if you attack it with melee weapons. +- Fixed enemy crews not being able to switch between turrets and being very reluctant to operate multiple turrets at the same time. +- Fixed reputation reward text sometimes overflowing in the round summary (e.g. when the mission modifies both the husk cult and clown rep). +- Fixed characters holding and eating bananas weirdly. +- Fixed all friendly characters using the "hostage" dialog and all hostile characters the "bandit" dialog in abandoned outposts. +- Fixed characters sometimes becoming briefly immobilized (as if stunned) when the surface of the water rises up to the character's chest. +- Fixed cultist hood overlapping with the exosuit. +- Fixed exosuit and FB3000 not being tagged as "provocative" (meaning enemies didn't care about the sounds they make or the lights on them). +- Adjusted FB3000 fabrication recipe (the previous required so many materials they don't fit in the fabricator's input slots). +- Fixed PUCS not having an AI target unlike all other diving suits. +- Fixed sonar monitor's "sonar circle" overlapping with the control panel on some resolutions / HUD scales. +- Fixed occasional invisible barriers around alien ruins. +- Fixed protein bars only healing 1/60 of the intended amount. +- Fixed "Engineers_are_special" event no longer appearing. +- Fixed characters sometimes being able pass through level walls by swimming down through the broken floor of a wreck. +- Fixed "acquire a wrench" popup appearing multiple times in the mechanic tutorial. +- Fixed black squares on docking ports and hatches. +- Fixed crashing when trying to increment the version number of a mod whose version number consists of only numbers (i.e. no periods) or is empty. +- Fixed hireable cultists and clowns being the wrong way around (i.e. high clown rep allowed you to hire cultists and vice versa). +- Fixed wikiimage_sub and wikiimage_character crashing the game if the file is being used by another process. +- Fixed wikiimage_sub not sorting the entities the same way as the sub editor and game screen. +- Fixed status monitor displaying very small amounts of water in linked hulls as 1%. +- Faction reputation is reset after finishing the campaign. +- Fixed colony docking modules spawning with a bit of water in them. +- Fixed minerals sometimes spawning in normal caves in abyss mining missions. Happened if no abyss islands with caves happened to generate - now we always generate a cave in at least one of the islands. +- Fixed lights on the items the character is wearing being visible when inside a clown crate. +- Fixed mudraptor eggs (or other items set to be damaged by repair tools) not being damaged by flamers. +- Fixed handheld sonars not working in the end levels. +- Fixed rebinding the Use key not working in the sub editor. +- Fixed Ctrl+A not selecting connected wires in the sub editor. +- Fixed ability to climb ladders while lying in bed. +- Fabricator input slot tooltips don't show duplicate item names when the item can be crafted from multiple different items with the same name (e.g. petraptor egg can be crafted from 3 different egg items, all called "mudraptor egg"). +- Fixed attacking others with the husk appendage not healing the user. +- Fixed pet name tag getting stuck mid-air when equipped. +- Fixed next round's missions not being displayed in the round summary when leaving a location that has missions (e.g. outpost with a jailbreak mission). +- Fixed inability to hire Jacov Subra if you miss/ignore the event the first time you get it. +- Outpost generator only takes walls with a collider into account when determining the bounds of the modules. Fixes husk modules being placed unnecessarily far from the outpost due to the decorative structures outside the door, leaving a very short hallway between the modules. +- Fixed characters sometimes dying from barotrauma despite the pressure icon not being visible. Happened when the pressure was just above the lethal threshold, but not as high as outside the sub. +- Fixed character interact texts (like "[H] Heal") not changing when you change the language. +- Fixed treatment suggestion for husk infection being shown when wearing zealot robes. +- Fixed inability to fabricate high-quality nuclear depth charges. +- Fixed exosuits getting autofilled with batteries. +- Fixed exosuits' lights not turning off when the wearer dies. +- Fixed 0% grenades (stun grenade and fixfoam) detonating if a player has one in their inventory when a mission starts. +- Fixed pre-unlocked talents not being visible client-side on the special faction NPCs hired during the round. +- Fixed escort and cargo missions sometimes leading to an abandoned outpost even if there's an inhabited one available. +- Fixed characters (ragdolls) sometimes getting stuck to the corners near platforms. + +Modding: +- Fixed thalamus items fading from the prefab color to a darker tint, disregarding the actual sprite color of the item. +- Fixed LevelTrigger statuseffects not doing anything when triggered by a submarine. +- Added a scrollbar to the campaign setup's crew tab to make it work properly when there's more than 3 initial crew members. +- Support for using OnWearing in Containable StatusEffects. +- Fixed ability to "relaunch" a projectile that has already been launched or that's stuck to some target using status effects, which lead to various strange results. +- Fixed ItemContainer StatusEffects working in a different way than other effects, applying the effect separately to each target. Prevents e.g. having an effect that does something to an item if a condition is met on the character wearing it. + --------------------------------------------------------------------------------------------------------- v1.0.9.0 --------------------------------------------------------------------------------------------------------- @@ -77,12 +273,7 @@ v1.0.2.0 - Fixed missing "place in ceiling" text in beacon station save dialog. - Fixed basic depth charges being cheaper than intended (only 30 mk). - Fixed inability to make lights blink at a high frequency by rapidly turning them on and off with e.g. oscillators. -- Fixed red glow around the light switch's green button. -- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. -- Fixed "skedaddle" not giving a 10% movement boost like the description says. -- Fixed acid burns not having a cause of death text. - Fixed ranged weapons emitting particles in the wrong direction. There haven't been any changes to this code in years, so it must've been an issue for a long time, I guess we just never noticed because no gun before the scrap cannon emitted particles with a noticeable velocity? -- Fixed banana being held weirldy. - Fixed a pathfinding issue that often made bots swim against cave walls. - Fixed inability to join servers with a submarine switch/purchase vote running. - Fixed votes passing if the client who initiated them disconnects before anyone else votes. @@ -104,7 +295,7 @@ v1.0.1.0 - Fixed "skedaddle" not giving a 10% movement boost like the description says. - Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. - Fixed red glow around the light switch's green button. -- Fixed banana being held weirldy. +- Fixed banana being held weirdly. - Fixed inability to hold a captain's pipe or cigar in your left hand. - Fixed ready checks not working. @@ -126,7 +317,7 @@ Faction overhaul: - Added a 3rd talent tree, "Politician", for the Captain. Focused around faction relations and reputation. Endgame: -- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels or radiation. +- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels of radiation. - New types of enemies/bosses. - Some new events to foreshadow the ending during the course of the campaign. diff --git a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs index fd6af2fa0..c47fdb10d 100644 --- a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs +++ b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs @@ -14,7 +14,7 @@ namespace Steamworks internal struct CallResult : INotifyCompletion where T : struct, ICallbackData { SteamAPICall_t call; - ISteamUtils utils; + ISteamUtils? utils; bool server; public CallResult( SteamAPICall_t call, bool server ) @@ -43,6 +43,8 @@ namespace Steamworks /// public T? GetResult() { + if (utils is null) { return null; } + bool failed = false; if ( !utils.IsAPICallCompleted( call, ref failed ) || failed ) return null; @@ -76,6 +78,8 @@ namespace Steamworks { get { + if (utils is null) { return true; } + bool failed = false; if ( utils.IsAPICallCompleted( call, ref failed ) || failed ) return true; diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs index 28c80c246..aedff442f 100644 --- a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs @@ -4,7 +4,7 @@ namespace Steamworks { public class AuthTicket : IDisposable { - public byte[] Data; + public byte[]? Data; public uint Handle; public bool Canceled { get; private set; } @@ -17,7 +17,7 @@ namespace Steamworks { if (Handle != 0) { - SteamUser.Internal.CancelAuthTicket(Handle); + SteamUser.Internal?.CancelAuthTicket(Handle); } Handle = 0; diff --git a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs index 680c54238..a0a722464 100644 --- a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs +++ b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs @@ -26,7 +26,7 @@ namespace Steamworks /// Params are : [Callback Type] [Callback Contents] [server] /// /// - public static Action OnDebugCallback; + public static Action? OnDebugCallback; /// /// Called if an exception happens during a callback/callresult. @@ -34,7 +34,7 @@ namespace Steamworks /// async.. and can fail silently. With this hooked you won't be stuck wondering /// what happened. /// - public static Action OnException; + public static Action? OnException; #region interop [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_Init", CallingConvention = CallingConvention.Cdecl )] @@ -288,7 +288,7 @@ namespace Steamworks /// /// Install a global callback. The passed function will get called if it's all good. /// - internal static void Install( Action p, bool server = false ) where T : ICallbackData + internal static void Install( Action p, bool server = false ) where T : struct, ICallbackData { var t = default( T ); var type = t.CallbackType; diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj index 474798281..26bfe8f64 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj @@ -5,10 +5,15 @@ $(DefineConstants);PLATFORM_POSIX64;PLATFORM_POSIX;PLATFORM_64 netstandard2.1 true - 8.0 + latest true false Steamworks + enable + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 94741525f..29d8dfcd1 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -10,6 +10,7 @@ true Steamworks AnyCPU;x64 + enable @@ -48,6 +49,10 @@ 1701;1702;1591;1587 + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index baa03e68f..3e5e1c326 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -76,7 +76,7 @@ namespace Steamworks private static extern Utf8StringPointer _GetCurrentGameLanguage( IntPtr self ); #endregion - internal string GetCurrentGameLanguage() + internal string? GetCurrentGameLanguage() { var returnValue = _GetCurrentGameLanguage( Self ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index 9adbeae02..b8c24ac7d 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -40,13 +40,13 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItems", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ); + private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ); #endregion /// /// Copies the contents of a result set into a flat array. The specific contents of the result set depend on which query which was used. /// - internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ) + internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ) { var returnValue = _GetResultItems( Self, resultHandle, pOutItemsArray, ref punOutItemsArraySize ); return returnValue; @@ -55,10 +55,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItemProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; @@ -311,10 +311,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionIDs", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ); + private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ); #endregion - internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ) + internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ) { var returnValue = _GetItemDefinitionIDs( Self, pItemDefIDs, ref punItemDefIDsArraySize ); return returnValue; @@ -323,10 +323,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; diff --git a/Libraries/Facepunch.Steamworks/Networking/Connection.cs b/Libraries/Facepunch.Steamworks/Networking/Connection.cs index f5f75e5d2..0eb4aafce 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Connection.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Connection.cs @@ -24,7 +24,7 @@ namespace Steamworks.Data /// public Result Accept() { - return SteamNetworkingSockets.Internal.AcceptConnection( this ); + return SteamNetworkingSockets.Internal?.AcceptConnection( this ) ?? Result.Fail; } /// @@ -33,7 +33,7 @@ namespace Steamworks.Data /// public bool Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) { - return SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); } /// @@ -41,8 +41,8 @@ namespace Steamworks.Data /// public long UserData { - get => SteamNetworkingSockets.Internal.GetConnectionUserData( this ); - set => SteamNetworkingSockets.Internal.SetConnectionUserData( this, value ); + get => SteamNetworkingSockets.Internal?.GetConnectionUserData( this ) ?? 0; + set => SteamNetworkingSockets.Internal?.SetConnectionUserData( this, value ); } /// @@ -52,13 +52,13 @@ namespace Steamworks.Data { get { - if ( !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) + if ( SteamNetworkingSockets.Internal is null || !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) return "ERROR"; return strVal; } - set => SteamNetworkingSockets.Internal.SetConnectionName( this, value ); + set => SteamNetworkingSockets.Internal?.SetConnectionName( this, value ); } /// @@ -67,7 +67,7 @@ namespace Steamworks.Data public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable ) { long messageNumber = 0; - return SteamNetworkingSockets.Internal.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ); + return SteamNetworkingSockets.Internal?.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ) ?? Result.Fail; } /// @@ -107,16 +107,16 @@ namespace Steamworks.Data /// Flush any messages waiting on the Nagle timer and send them at the next transmission /// opportunity (often that means right now). /// - public Result Flush() => SteamNetworkingSockets.Internal.FlushMessagesOnConnection( this ); + public Result Flush() => SteamNetworkingSockets.Internal?.FlushMessagesOnConnection( this ) ?? Result.Fail; /// /// Returns detailed connection stats in text format. Useful /// for dumping to a log, etc. /// /// Plain text connection info - public string DetailedStatus() + public string? DetailedStatus() { - if ( SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) + if ( SteamNetworkingSockets.Internal is null || SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) return null; return strVal; diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs index 2007cc777..1ac65e609 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs @@ -9,7 +9,7 @@ namespace Steamworks /// /// An optional interface to use instead of deriving /// - public IConnectionManager Interface { get; set; } + public IConnectionManager? Interface { get; set; } /// /// The actual connection we're managing @@ -94,6 +94,8 @@ namespace Steamworks public void Receive( int bufferSize = 32 ) { + if (SteamNetworkingSockets.Internal is null) { return; } + int processed = 0; IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); diff --git a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs index 270bf5a9a..f118bdde0 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs @@ -107,10 +107,11 @@ namespace Steamworks.Data /// /// We override tostring to provide a sensible representation /// - public override string ToString() + public override string? ToString() { var id = this; - SteamNetworkingUtils.Internal.SteamNetworkingIdentity_ToString( ref id, out var str ); + string? str = null; + SteamNetworkingUtils.Internal?.SteamNetworkingIdentity_ToString( ref id, out str ); return str; } diff --git a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs index 9aceca533..2fd3c926b 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs @@ -25,15 +25,16 @@ namespace Steamworks.Data public static NetPingLocation? TryParseFromString( string str ) { var result = default( NetPingLocation ); - if ( !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) + if ( SteamNetworkingUtils.Internal is null || !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) return null; return result; } - public override string ToString() + public override string? ToString() { - SteamNetworkingUtils.Internal.ConvertPingLocationToString( ref this, out var strVal ); + string? strVal = null; + SteamNetworkingUtils.Internal?.ConvertPingLocationToString( ref this, out strVal ); return strVal; } @@ -61,7 +62,7 @@ namespace Steamworks.Data /// You are looking for the "ticketgen" library. public int EstimatePingTo( NetPingLocation target ) { - return SteamNetworkingUtils.Internal.EstimatePingTimeBetweenTwoLocations( ref this, ref target ); + return SteamNetworkingUtils.Internal?.EstimatePingTimeBetweenTwoLocations( ref this, ref target ) ?? Defines.k_nSteamNetworkingPing_Failed; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/Socket.cs b/Libraries/Facepunch.Steamworks/Networking/Socket.cs index 334f5893e..730e63324 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Socket.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Socket.cs @@ -1,4 +1,5 @@  +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Steamworks.Data @@ -17,10 +18,11 @@ namespace Steamworks.Data /// public bool Close() { - return SteamNetworkingSockets.Internal.CloseListenSocket( Id ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseListenSocket( Id ); } - public SocketManager Manager + [DisallowNull] + public SocketManager? Manager { get => SteamNetworkingSockets.GetSocketManager( Id ); set => SteamNetworkingSockets.SetSocketManager( Id, value ); diff --git a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs index 5c585faec..1d9721cf4 100644 --- a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public partial class SocketManager { - public ISocketManager Interface { get; set; } + public ISocketManager? Interface { get; set; } public List Connecting = new List(); public List Connected = new List(); @@ -26,12 +26,12 @@ namespace Steamworks internal void Initialize() { - pollGroup = SteamNetworkingSockets.Internal.CreatePollGroup(); + pollGroup = SteamNetworkingSockets.Internal?.CreatePollGroup() ?? default; } public bool Close() { - if ( SteamNetworkingSockets.Internal.IsValid ) + if ( SteamNetworkingSockets.Internal is { IsValid: true } ) { SteamNetworkingSockets.Internal.DestroyPollGroup( pollGroup ); Socket.Close(); @@ -94,7 +94,7 @@ namespace Steamworks /// public virtual void OnConnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, pollGroup ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, pollGroup ); Interface?.OnConnected( connection, info ); } @@ -104,7 +104,7 @@ namespace Steamworks /// public virtual void OnDisconnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, 0 ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, 0 ); connection.Close(); @@ -121,7 +121,7 @@ namespace Steamworks try { - processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ); + processed = SteamNetworkingSockets.Internal?.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ) ?? 0; for ( int i = 0; i < processed; i++ ) { diff --git a/Libraries/Facepunch.Steamworks/ServerList/Base.cs b/Libraries/Facepunch.Steamworks/ServerList/Base.cs index 9a5cb1125..eaf66449a 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Base.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Base.cs @@ -11,7 +11,7 @@ namespace Steamworks.ServerList { #region ISteamMatchmakingServers - internal static ISteamMatchmakingServers Internal => SteamMatchmakingServers.Internal; + internal static ISteamMatchmakingServers? Internal => SteamMatchmakingServers.Internal; #endregion @@ -23,17 +23,17 @@ namespace Steamworks.ServerList /// /// When a new server is added, this function will get called /// - public Action OnChanges; + public Action? OnChanges; /// /// Called for every responsive server /// - public Action OnResponsiveServer; + public Action? OnResponsiveServer; /// /// Called for every unresponsive server /// - public Action OnUnresponsiveServer; + public Action? OnUnresponsiveServer; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -98,7 +98,7 @@ namespace Steamworks.ServerList return true; } - public virtual void Cancel() => Internal.CancelQuery( request ); + public virtual void Cancel() => Internal?.CancelQuery( request ); // Overrides internal abstract void LaunchQuery(); @@ -117,8 +117,8 @@ namespace Steamworks.ServerList #endregion - internal int Count => Internal.GetServerCount( request ); - internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal.IsRefreshing( request ); + internal int Count => Internal?.GetServerCount( request ) ?? 0; + internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal != null && Internal.IsRefreshing( request ); internal List watchList = new List(); internal int LastCount = 0; @@ -134,7 +134,7 @@ namespace Steamworks.ServerList if ( request.Value != IntPtr.Zero ) { Cancel(); - Internal.ReleaseRequest( request ); + Internal?.ReleaseRequest( request ); request = IntPtr.Zero; } } @@ -166,6 +166,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); if ( info.HadSuccessfulResponse ) { @@ -181,6 +183,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); OnServer( ServerInfo.From( info ), info.HadSuccessfulResponse ); return true; diff --git a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs index 6f1ffa3c0..e71394570 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFavoritesServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs index eb66a692b..60d72e31c 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFriendsServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/History.cs b/Libraries/Facepunch.Steamworks/ServerList/History.cs index 55ccc166c..3d059767e 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/History.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/History.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestHistoryServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs index 86e599e66..c493b26fa 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs @@ -10,8 +10,8 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); - request = Internal.RequestInternetServerList( AppId.Value, filters, (uint)filters.Length, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs index 746887335..ed5d476c8 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } request = Internal.RequestLANServerList( AppId.Value, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamApps.cs b/Libraries/Facepunch.Steamworks/SteamApps.cs index c201882b9..80af70a5e 100644 --- a/Libraries/Facepunch.Steamworks/SteamApps.cs +++ b/Libraries/Facepunch.Steamworks/SteamApps.cs @@ -13,7 +13,7 @@ namespace Steamworks /// public class SteamApps : SteamSharedClass { - internal static ISteamApps Internal => Interface as ISteamApps; + internal static ISteamApps? Internal => Interface as ISteamApps; internal override void InitializeInterface( bool server ) { @@ -29,7 +29,7 @@ namespace Steamworks /// /// posted after the user gains ownership of DLC and that DLC is installed /// - public static event Action OnDlcInstalled; + public static event Action? OnDlcInstalled; /// /// posted after the user gains executes a Steam URL with command line or query parameters @@ -37,61 +37,63 @@ namespace Steamworks /// while the game is already running. The new params can be queried /// with GetLaunchQueryParam and GetLaunchCommandLine /// - public static event Action OnNewLaunchParameters; + public static event Action? OnNewLaunchParameters; /// /// Checks if the active user is subscribed to the current App ID /// - public static bool IsSubscribed => Internal.BIsSubscribed(); + public static bool IsSubscribed => Internal != null && Internal.BIsSubscribed(); /// /// Check if user borrowed this game via Family Sharing, If true, call GetAppOwner() to get the lender SteamID /// - public static bool IsSubscribedFromFamilySharing => Internal.BIsSubscribedFromFamilySharing(); + public static bool IsSubscribedFromFamilySharing => Internal != null && Internal.BIsSubscribedFromFamilySharing(); /// /// Checks if the license owned by the user provides low violence depots. /// Low violence depots are useful for copies sold in countries that have content restrictions /// - public static bool IsLowViolence => Internal.BIsLowViolence(); + public static bool IsLowViolence => Internal != null && Internal.BIsLowViolence(); /// /// Checks whether the current App ID license is for Cyber Cafes. /// - public static bool IsCybercafe => Internal.BIsCybercafe(); + public static bool IsCybercafe => Internal != null && Internal.BIsCybercafe(); /// /// CChecks if the user has a VAC ban on their account /// - public static bool IsVACBanned => Internal.BIsVACBanned(); + public static bool IsVACBanned => Internal != null && Internal.BIsVACBanned(); /// /// Gets the current language that the user has set. /// This falls back to the Steam UI language if the user hasn't explicitly picked a language for the title. /// - public static string GameLanguage => Internal.GetCurrentGameLanguage(); + public static string? GameLanguage => Internal?.GetCurrentGameLanguage(); /// /// Gets a list of the languages the current app supports. /// - public static string[] AvailableLanguages => Internal.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); + public static string[]? AvailableLanguages => Internal?.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); /// /// Checks if the active user is subscribed to a specified AppId. /// Only use this if you need to check ownership of another game related to yours, a demo for example. /// - public static bool IsSubscribedToApp( AppId appid ) => Internal.BIsSubscribedApp( appid.Value ); + public static bool IsSubscribedToApp( AppId appid ) => Internal != null && Internal.BIsSubscribedApp( appid.Value ); /// /// Checks if the user owns a specific DLC and if the DLC is installed /// - public static bool IsDlcInstalled( AppId appid ) => Internal.BIsDlcInstalled( appid.Value ); + public static bool IsDlcInstalled( AppId appid ) => Internal != null && Internal.BIsDlcInstalled( appid.Value ); /// /// Returns the time of the purchase of the app /// public static DateTime PurchaseTime( AppId appid = default ) { + if (Internal is null) { return default; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -103,7 +105,7 @@ namespace Steamworks /// This function will return false for users who have a retail or other type of license /// Before using, please ask your Valve technical contact how to package and secure your free weekened /// - public static bool IsSubscribedFromFreeWeekend => Internal.BIsSubscribedFromFreeWeekend(); + public static bool IsSubscribedFromFreeWeekend => Internal != null && Internal.BIsSubscribedFromFreeWeekend(); /// /// Returns metadata for all available DLC @@ -113,8 +115,12 @@ namespace Steamworks var appid = default( AppId ); var available = false; - for ( int i = 0; i < Internal.GetDLCCount(); i++ ) + if (Internal is null) { yield break; } + + int dlcCount = Internal.GetDLCCount(); + for ( int i = 0; i < dlcCount; i++ ) { + if (Internal is null) { yield break; } if ( !Internal.BGetDLCDataByIndex( i, ref appid, ref available, out var strVal ) ) continue; @@ -130,21 +136,21 @@ namespace Steamworks /// /// Install/Uninstall control for optional DLC /// - public static void InstallDlc( AppId appid ) => Internal.InstallDLC( appid.Value ); + public static void InstallDlc( AppId appid ) => Internal?.InstallDLC( appid.Value ); /// /// Install/Uninstall control for optional DLC /// - public static void UninstallDlc( AppId appid ) => Internal.UninstallDLC( appid.Value ); + public static void UninstallDlc( AppId appid ) => Internal?.UninstallDLC( appid.Value ); /// /// Returns null if we're not on a beta branch, else the name of the branch /// - public static string CurrentBetaName + public static string? CurrentBetaName { get { - if ( !Internal.GetCurrentBetaName( out var strVal ) ) + if ( Internal is null || !Internal.GetCurrentBetaName( out var strVal ) ) return null; return strVal; @@ -157,13 +163,15 @@ namespace Steamworks /// If you detect the game is out-of-date(for example, by having the client detect a version mismatch with a server), /// you can call use MarkContentCorrupt to force a verify, show a message to the user, and then quit. /// - public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal.MarkContentCorrupt( missingFilesOnly ); + public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal?.MarkContentCorrupt( missingFilesOnly ); /// /// Gets a list of all installed depots for a given App ID in mount order /// public static IEnumerable InstalledDepots( AppId appid = default ) { + if (Internal is null) { yield break; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -180,12 +188,12 @@ namespace Steamworks /// Gets the install folder for a specific AppID. /// This works even if the application is not installed, based on where the game would be installed with the default Steam library location. /// - public static string AppInstallDir( AppId appid = default ) + public static string? AppInstallDir( AppId appid = default ) { if ( appid == 0 ) appid = SteamClient.AppId; - if ( Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) + if ( Internal is null || Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) return null; return strVal; @@ -194,12 +202,12 @@ namespace Steamworks /// /// The app may not actually be owned by the current user, they may have it left over from a free weekend, etc. /// - public static bool IsAppInstalled( AppId appid ) => Internal.BIsAppInstalled( appid.Value ); + public static bool IsAppInstalled( AppId appid ) => Internal != null && Internal.BIsAppInstalled( appid.Value ); /// /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed.. /// - public static SteamId AppOwner => Internal.GetAppOwner().Value; + public static SteamId AppOwner => Internal?.GetAppOwner().Value ?? default; /// /// Gets the associated launch parameter if the game is run via steam://run/appid/?param1=value1;param2=value2;param3=value3 etc. @@ -207,7 +215,7 @@ namespace Steamworks /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, /// but it is advised that you not param names beginning with an underscore for your own features. /// - public static string GetLaunchParam( string param ) => Internal.GetLaunchQueryParam( param ); + public static string? GetLaunchParam( string param ) => Internal?.GetLaunchQueryParam( param ); /// /// Gets the download progress for optional DLC. @@ -217,7 +225,7 @@ namespace Steamworks ulong punBytesDownloaded = 0; ulong punBytesTotal = 0; - if ( !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) + if ( Internal is null || !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) return default; return new DownloadProgress { BytesDownloaded = punBytesDownloaded, BytesTotal = punBytesTotal, Active = true }; @@ -227,7 +235,7 @@ namespace Steamworks /// Gets the buildid of this app, may change at any time based on backend updates to the game. /// Defaults to 0 if you're not running a build downloaded from steam. /// - public static int BuildId => Internal.GetAppBuildId(); + public static int BuildId => Internal?.GetAppBuildId() ?? 0; /// @@ -236,6 +244,7 @@ namespace Steamworks /// public static async Task GetFileDetailsAsync( string filename ) { + if (Internal is null) { return null; } var r = await Internal.GetFileDetails( filename ); if ( !r.HasValue || r.Value.Result != Result.OK ) @@ -257,11 +266,12 @@ namespace Steamworks /// path and not be placed on the OS command line, you must set a value in your app's /// configuration on Steam. Ask Valve for help with this. /// - public static string CommandLine + public static string? CommandLine { get { - Internal.GetLaunchCommandLine( out var strVal ); + string? strVal = null; + Internal?.GetLaunchCommandLine( out strVal ); return strVal; } } diff --git a/Libraries/Facepunch.Steamworks/SteamClient.cs b/Libraries/Facepunch.Steamworks/SteamClient.cs index fb44cff44..31ef55045 100644 --- a/Libraries/Facepunch.Steamworks/SteamClient.cs +++ b/Libraries/Facepunch.Steamworks/SteamClient.cs @@ -124,7 +124,7 @@ namespace Steamworks /// very good experience for the player and you could be preventing them from accessing APIs that do not /// need a live connection to Steam. /// - public static bool IsLoggedOn => SteamUser.Internal.BLoggedOn(); + public static bool IsLoggedOn => SteamUser.Internal != null && SteamUser.Internal.BLoggedOn(); /// /// Gets the Steam ID of the account currently logged into the Steam client. This is @@ -132,18 +132,18 @@ namespace Steamworks /// A Steam ID is a unique identifier for a Steam accounts, Steam groups, Lobbies and Chat /// rooms, and used to differentiate users in all parts of the Steamworks API. /// - public static SteamId SteamId => SteamUser.Internal.GetSteamID(); + public static SteamId SteamId => SteamUser.Internal?.GetSteamID() ?? default; /// /// returns the local players name - guaranteed to not be NULL. /// this is the same name as on the users community profile page /// - public static string Name => SteamFriends.Internal.GetPersonaName(); + public static string? Name => SteamFriends.Internal?.GetPersonaName(); /// /// gets the status of the current user /// - public static FriendState State => SteamFriends.Internal.GetPersonaState(); + public static FriendState State => SteamFriends.Internal?.GetPersonaState() ?? FriendState.Offline; /// /// returns the appID of the current process diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index 36a57e6cc..bcc457525 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamFriends : SteamClientClass { - internal static ISteamFriends Internal => Interface as ISteamFriends; + internal static ISteamFriends? Internal => Interface as ISteamFriends; internal override void InitializeInterface( bool server ) { @@ -23,7 +23,7 @@ namespace Steamworks InstallEvents(); } - static Dictionary richPresence; + static Dictionary? richPresence; internal void InstallEvents() { @@ -40,42 +40,42 @@ namespace Steamworks /// Called when chat message has been received from a friend. You'll need to turn on /// ListenForFriendsMessages to recieve this. (friend, msgtype, message) /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; /// /// called when a friends' status changes /// - public static event Action OnPersonaStateChange; + public static event Action? OnPersonaStateChange; /// /// Called when the user tries to join a game from their friends list /// rich presence will have been set with the "connect" key which is set here /// - public static event Action OnGameRichPresenceJoinRequested; + public static event Action? OnGameRichPresenceJoinRequested; /// /// Posted when game overlay activates or deactivates /// the game can use this to be pause or resume single player games /// - public static event Action OnGameOverlayActivated; + public static event Action? OnGameOverlayActivated; /// /// Called when the user tries to join a different game server from their friends list /// game client should attempt to connect to specified server when this is received /// - public static event Action OnGameServerChangeRequested; + public static event Action? OnGameServerChangeRequested; /// /// Called when the user tries to join a lobby from their friends list /// game client should attempt to connect to specified lobby when this is received /// - public static event Action OnGameLobbyJoinRequested; + public static event Action? OnGameLobbyJoinRequested; /// /// Callback indicating updated data about friends rich presence information /// - public static event Action OnFriendRichPresenceUpdate; + public static event Action? OnFriendRichPresenceUpdate; static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { @@ -86,7 +86,7 @@ namespace Steamworks using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; - var len = Internal.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ); + var len = Internal?.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ) ?? 0; if ( len == 0 && type == ChatEntryType.Invalid ) return; @@ -99,15 +99,18 @@ namespace Steamworks private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { - for ( int i=0; i GetFriends() @@ -142,24 +145,33 @@ namespace Steamworks public static IEnumerable GetPlayedWith() { - for ( int i = 0; i < Internal.GetCoplayFriendCount(); i++ ) + if (Internal is null) { yield break; } + int friendCount = Internal.GetCoplayFriendCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetCoplayFriend( i ) ); } } public static IEnumerable GetFromSource( SteamId steamid ) { - for ( int i = 0; i < Internal.GetFriendCountFromSource( steamid ); i++ ) - { + if (Internal is null) { yield break; } + int friendCount = Internal.GetFriendCountFromSource( steamid ); + for ( int i = 0; i < friendCount; i++ ) + { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetFriendFromSourceByIndex( steamid, i ) ); } } public static IEnumerable GetClans() { - for (int i = 0; i < Internal.GetClanCount(); i++) + if (Internal is null) { yield break; } + int friendCount = Internal.GetClanCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Clan( Internal.GetClanByIndex( i ) ); } } @@ -174,7 +186,7 @@ namespace Steamworks /// "stats", /// "achievements". /// - public static void OpenOverlay( string type ) => Internal.ActivateGameOverlay( type ); + public static void OpenOverlay( string type ) => Internal?.ActivateGameOverlay( type ); /// /// "steamid" - Opens the overlay web browser to the specified user or groups profile. @@ -187,35 +199,35 @@ namespace Steamworks /// "friendrequestaccept" - Opens the overlay in minimal mode prompting the user to accept an incoming friend invite. /// "friendrequestignore" - Opens the overlay in minimal mode prompting the user to ignore an incoming friend invite. /// - public static void OpenUserOverlay( SteamId id, string type ) => Internal.ActivateGameOverlayToUser( type, id ); + public static void OpenUserOverlay( SteamId id, string type ) => Internal?.ActivateGameOverlayToUser( type, id ); /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// - public static void OpenStoreOverlay( AppId id ) => Internal.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); + public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); /// /// Activates Steam Overlay web browser directly to the specified URL. /// - public static void OpenWebOverlay( string url, bool modal = false ) => Internal.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); + public static void OpenWebOverlay( string url, bool modal = false ) => Internal?.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); /// /// Activates the Steam Overlay to open the invite dialog. Invitations sent from this dialog will be for the provided lobby. /// - public static void OpenGameInviteOverlay( SteamId lobby ) => Internal.ActivateGameOverlayInviteDialog( lobby ); + public static void OpenGameInviteOverlay( SteamId lobby ) => Internal?.ActivateGameOverlayInviteDialog( lobby ); /// /// Mark a target user as 'played with'. /// NOTE: The current user must be in game with the other player for the association to work. /// - public static void SetPlayedWith( SteamId steamid ) => Internal.SetPlayedWith( steamid ); + public static void SetPlayedWith( SteamId steamid ) => Internal?.SetPlayedWith( steamid ); /// /// Requests the persona name and optionally the avatar of a specified user. /// NOTE: It's a lot slower to download avatars and churns the local cache, so if you don't need avatars, don't request them. /// returns true if we're fetching the data, false if we already have it /// - public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal.RequestUserInformation( steamid, nameonly ); + public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal != null && Internal.RequestUserInformation( steamid, nameonly ); internal static async Task CacheUserInformationAsync( SteamId steamid, bool nameonly ) @@ -239,18 +251,21 @@ namespace Steamworks public static async Task GetSmallAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } public static async Task GetMediumAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } public static async Task GetLargeAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); var imageid = Internal.GetLargeFriendAvatar( steamid ); @@ -268,8 +283,10 @@ namespace Steamworks /// /// Find a rich presence value by key for current user. Will be null if not found. /// - public static string GetRichPresence( string key ) + public static string? GetRichPresence( string key ) { + if (richPresence is null) { return null; } + if ( richPresence.TryGetValue( key, out var val ) ) return val; @@ -281,6 +298,8 @@ namespace Steamworks /// public static bool SetRichPresence( string key, string value ) { + if (richPresence is null || Internal is null) { return false; } + bool success = Internal.SetRichPresence( key, value ); if ( success ) @@ -294,8 +313,8 @@ namespace Steamworks /// public static void ClearRichPresence() { - richPresence.Clear(); - Internal.ClearRichPresence(); + richPresence?.Clear(); + Internal?.ClearRichPresence(); } static bool _listenForFriendsMessages; @@ -312,20 +331,22 @@ namespace Steamworks set { _listenForFriendsMessages = value; - Internal.SetListenForFriendsMessages( value ); + Internal?.SetListenForFriendsMessages( value ); } } public static async Task IsFollowing(SteamId steamID) { + if (Internal is null) { return false; } var r = await Internal.IsFollowing(steamID); - return r.Value.IsFollowing; + return r?.IsFollowing ?? false; } public static async Task GetFollowerCount(SteamId steamID) { + if (Internal is null) { return 0; } var r = await Internal.GetFollowerCount(steamID); - return r.Value.Count; + return r?.Count ?? 0; } public static async Task GetFollowingList() @@ -337,6 +358,7 @@ namespace Steamworks do { + if (Internal is null) { break; } if ( (result = await Internal.EnumerateFollowingList((uint)resultCount)) != null) { resultCount += result.Value.ResultsReturned; diff --git a/Libraries/Facepunch.Steamworks/SteamInput.cs b/Libraries/Facepunch.Steamworks/SteamInput.cs index d55e523c4..3db2293b4 100644 --- a/Libraries/Facepunch.Steamworks/SteamInput.cs +++ b/Libraries/Facepunch.Steamworks/SteamInput.cs @@ -5,7 +5,7 @@ namespace Steamworks { public class SteamInput : SteamClientClass { - internal static ISteamInput Internal => Interface as ISteamInput; + internal static ISteamInput? Internal => Interface as ISteamInput; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks /// public static void RunFrame() { - Internal.RunFrame(); + Internal?.RunFrame(); } static readonly InputHandle_t[] queryArray = new InputHandle_t[STEAM_CONTROLLER_MAX_COUNT]; @@ -34,7 +34,7 @@ namespace Steamworks { get { - var num = Internal.GetConnectedControllers( queryArray ); + var num = Internal?.GetConnectedControllers( queryArray ) ?? 0; for ( int i = 0; i < num; i++ ) { @@ -52,8 +52,10 @@ namespace Steamworks /// /// /// - public static string GetDigitalActionGlyph( Controller controller, string action ) + public static string? GetDigitalActionGlyph( Controller controller, string action ) { + if (Internal is null) { return null; } + InputActionOrigin origin = InputActionOrigin.None; Internal.GetDigitalActionOrigins( @@ -69,6 +71,8 @@ namespace Steamworks internal static Dictionary DigitalHandles = new Dictionary(); internal static InputDigitalActionHandle_t GetDigitalActionHandle( string name ) { + if (Internal is null) { return default; } + if ( DigitalHandles.TryGetValue( name, out var val ) ) return val; @@ -80,6 +84,8 @@ namespace Steamworks internal static Dictionary AnalogHandles = new Dictionary(); internal static InputAnalogActionHandle_t GetAnalogActionHandle( string name ) { + if (Internal is null) { return default; } + if ( AnalogHandles.TryGetValue( name, out var val ) ) return val; @@ -91,6 +97,8 @@ namespace Steamworks internal static Dictionary ActionSets = new Dictionary(); internal static InputActionSetHandle_t GetActionSetHandle( string name ) { + if (Internal is null) { return default; } + if ( ActionSets.TryGetValue( name, out var val ) ) return val; diff --git a/Libraries/Facepunch.Steamworks/SteamInventory.cs b/Libraries/Facepunch.Steamworks/SteamInventory.cs index f0a63c040..f45131dd1 100644 --- a/Libraries/Facepunch.Steamworks/SteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/SteamInventory.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public class SteamInventory : SteamSharedClass { - internal static ISteamInventory Internal => Interface as ISteamInventory; + internal static ISteamInventory? Internal => Interface as ISteamInventory; internal override void InitializeInterface( bool server ) { @@ -41,8 +41,8 @@ namespace Steamworks OnInventoryUpdated?.Invoke( r ); } - public static event Action OnInventoryUpdated; - public static event Action OnDefinitionsUpdated; + public static event Action? OnInventoryUpdated; + public static event Action? OnDefinitionsUpdated; static void LoadDefinitions() { @@ -79,7 +79,7 @@ namespace Steamworks LoadDefinitions(); } - Internal.LoadItemDefinitions(); + Internal?.LoadItemDefinitions(); } /// @@ -113,7 +113,7 @@ namespace Steamworks /// Try to find the definition that matches this definition ID. /// Uses a dictionary so should be about as fast as possible. /// - public static InventoryDef FindDefinition( InventoryDefId defId ) + public static InventoryDef? FindDefinition( InventoryDefId defId ) { if ( _defMap == null ) return null; @@ -124,15 +124,17 @@ namespace Steamworks return null; } - public static string Currency { get; internal set; } + public static string Currency { get; internal set; } = ""; - public static async Task GetDefinitionsWithPricesAsync() + public static async Task GetDefinitionsWithPricesAsync() { + if (Internal is null) { return null; } + var priceRequest = await Internal.RequestPrices(); - if ( !priceRequest.HasValue || priceRequest.Value.Result != Result.OK ) + if ( priceRequest?.Result != Result.OK ) return null; - Currency = priceRequest?.CurrencyUTF8(); + Currency = priceRequest.Value.CurrencyUTF8(); var num = Internal.GetNumItemsWithPrices(); @@ -153,15 +155,15 @@ namespace Steamworks /// /// We will try to keep this list of your items automatically up to date. /// - public static InventoryItem[] Items { get; internal set; } + public static InventoryItem[]? Items { get; internal set; } - public static InventoryDef[] Definitions { get; internal set; } - static Dictionary _defMap; + public static InventoryDef[]? Definitions { get; internal set; } + static Dictionary? _defMap; - internal static InventoryDef[] GetDefinitions() + internal static InventoryDef[]? GetDefinitions() { uint num = 0; - if ( !Internal.GetItemDefinitionIDs( null, ref num ) ) + if ( Internal is null || !Internal.GetItemDefinitionIDs( null, ref num ) ) return null; var defs = new InventoryDefId[num]; @@ -178,7 +180,7 @@ namespace Steamworks public static bool GetAllItems() { var sresult = Defines.k_SteamInventoryResultInvalid; - return Internal.GetAllItems( ref sresult ); + return Internal != null && Internal.GetAllItems( ref sresult ); } /// @@ -188,7 +190,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GetAllItems( ref sresult ) ) + if ( Internal is null || !Internal.GetAllItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -207,7 +209,7 @@ namespace Steamworks var defs = new InventoryDefId[] { target.Id }; var cnts = new uint[] { (uint)amount }; - if ( !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) + if ( Internal is null || !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -228,7 +230,7 @@ namespace Steamworks var sell = list.Select( x => x.Id ).ToArray(); var sellc = list.Select( x => (uint)1 ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -249,7 +251,7 @@ namespace Steamworks var sell = list.Select( x => x.Item.Id ).ToArray(); var sellc = list.Select( x => (uint) x.Quantity ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -285,7 +287,7 @@ namespace Steamworks var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) + if ( Internal is null || !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) return null; @@ -306,7 +308,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GrantPromoItems( ref sresult ) ) + if ( Internal is null || !Internal.GrantPromoItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -320,7 +322,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.TriggerItemDrop( ref sresult, id ) ) + if ( Internal is null || !Internal.TriggerItemDrop( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -334,7 +336,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.AddPromoItem( ref sresult, id ) ) + if ( Internal is null || !Internal.AddPromoItem( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -347,6 +349,8 @@ namespace Steamworks /// public static async Task StartPurchaseAsync( InventoryDef[] items ) { + if (Internal is null) { return null; } + var item_i = items.Select( x => x._id ).ToArray(); var item_q = items.Select( x => (uint)1 ).ToArray(); diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index ff3857a6f..df3ac42eb 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamMatchmaking : SteamClientClass { - internal static ISteamMatchmaking Internal => Interface as ISteamMatchmaking; + internal static ISteamMatchmaking? Internal => Interface as ISteamMatchmaking; internal override void InitializeInterface( bool server ) { @@ -70,6 +70,8 @@ namespace Steamworks static private unsafe void OnLobbyChatMessageRecievedAPI( LobbyChatMsg_t callback ) { + if (Internal is null) { return; } + SteamId steamid = default; ChatEntryType chatEntryType = default; using var buffer = Helpers.TakeMemory(); @@ -101,62 +103,62 @@ namespace Steamworks /// /// Someone invited you to a lobby /// - public static event Action OnLobbyInvite; + public static event Action? OnLobbyInvite; /// /// You joined a lobby /// - public static event Action OnLobbyEntered; + public static event Action? OnLobbyEntered; /// /// You created a lobby /// - public static event Action OnLobbyCreated; + public static event Action? OnLobbyCreated; /// /// A game server has been associated with the lobby /// - public static event Action OnLobbyGameCreated; + public static event Action? OnLobbyGameCreated; /// /// The lobby metadata has changed /// - public static event Action OnLobbyDataChanged; + public static event Action? OnLobbyDataChanged; /// /// The lobby member metadata has changed /// - public static event Action OnLobbyMemberDataChanged; + public static event Action? OnLobbyMemberDataChanged; /// /// The lobby member joined /// - public static event Action OnLobbyMemberJoined; + public static event Action? OnLobbyMemberJoined; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberLeave; + public static event Action? OnLobbyMemberLeave; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberDisconnected; + public static event Action? OnLobbyMemberDisconnected; /// /// The lobby member was kicked. The 3rd param is the user that kicked them. /// - public static event Action OnLobbyMemberKicked; + public static event Action? OnLobbyMemberKicked; /// /// The lobby member was banned. The 3rd param is the user that banned them. /// - public static event Action OnLobbyMemberBanned; + public static event Action? OnLobbyMemberBanned; /// /// A chat message was recieved from a member of a lobby /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; public static LobbyQuery CreateLobbyQuery() { return new LobbyQuery(); } @@ -165,6 +167,8 @@ namespace Steamworks /// public static async Task CreateLobbyAsync( int maxMembers = 100 ) { + if (Internal is null) { return null; } + var lobby = await Internal.CreateLobby( LobbyType.Invisible, maxMembers ); if ( !lobby.HasValue ) { return null; } @@ -176,6 +180,8 @@ namespace Steamworks /// public static async Task JoinLobbyAsync( SteamId lobbyId ) { + if (Internal is null) { return null; } + var lobby = await Internal.JoinLobby( lobbyId ); if ( !lobby.HasValue ) return null; @@ -187,6 +193,8 @@ namespace Steamworks /// public static IEnumerable GetFavoriteServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for( int i=0; i public static IEnumerable GetHistoryServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for ( int i = 0; i < count; i++ ) diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs index c1af07aee..a1d7d2c71 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs @@ -12,7 +12,7 @@ namespace Steamworks /// internal class SteamMatchmakingServers : SteamClientClass { - internal static ISteamMatchmakingServers Internal => Interface as ISteamMatchmakingServers; + internal static ISteamMatchmakingServers? Internal => Interface as ISteamMatchmakingServers; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/SteamMusic.cs b/Libraries/Facepunch.Steamworks/SteamMusic.cs index 1ef38f8bd..b0d6ef42d 100644 --- a/Libraries/Facepunch.Steamworks/SteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/SteamMusic.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamMusic : SteamClientClass { - internal static ISteamMusic Internal => Interface as ISteamMusic; + internal static ISteamMusic? Internal => Interface as ISteamMusic; internal override void InitializeInterface( bool server ) { @@ -33,50 +33,50 @@ namespace Steamworks /// /// Playback status changed /// - public static event Action OnPlaybackChanged; + public static event Action? OnPlaybackChanged; /// /// Volume changed, parameter is new volume /// - public static event Action OnVolumeChanged; + public static event Action? OnVolumeChanged; /// /// Checks if Steam Music is enabled /// - public static bool IsEnabled => Internal.BIsEnabled(); + public static bool IsEnabled => Internal != null && Internal.BIsEnabled(); /// /// true if a song is currently playing, paused, or queued up to play; otherwise false. /// - public static bool IsPlaying => Internal.BIsPlaying(); + public static bool IsPlaying => Internal != null && Internal.BIsPlaying(); /// /// Gets the current status of the Steam Music player /// - public static MusicStatus Status => Internal.GetPlaybackStatus(); + public static MusicStatus Status => Internal?.GetPlaybackStatus() ?? MusicStatus.Undefined; - public static void Play() => Internal.Play(); + public static void Play() => Internal?.Play(); - public static void Pause() => Internal.Pause(); + public static void Pause() => Internal?.Pause(); /// /// Have the Steam Music player play the previous song. /// - public static void PlayPrevious() => Internal.PlayPrevious(); + public static void PlayPrevious() => Internal?.PlayPrevious(); /// /// Have the Steam Music player skip to the next song /// - public static void PlayNext() => Internal.PlayNext(); + public static void PlayNext() => Internal?.PlayNext(); /// /// Gets/Sets the current volume of the Steam Music player /// public static float Volume { - get => Internal.GetVolume(); - set => Internal.SetVolume( value ); + get => Internal?.GetVolume() ?? 0f; + set => Internal?.SetVolume( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/SteamNetworking.cs index f211214c4..cadac81dd 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworking.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworking : SteamSharedClass { - internal static ISteamNetworking Internal => Interface as ISteamNetworking; + internal static ISteamNetworking? Internal => Interface as ISteamNetworking; internal override void InitializeInterface( bool server ) { @@ -35,26 +35,26 @@ namespace Steamworks /// This SteamId wants to send you a message. You should respond by calling AcceptP2PSessionWithUser /// if you want to recieve their messages /// - public static Action OnP2PSessionRequest; + public static Action? OnP2PSessionRequest; /// /// Called when packets can't get through to the specified user. /// All queued packets unsent at this point will be dropped, further attempts /// to send will retry making the connection (but will be dropped if we fail again). /// - public static Action OnP2PConnectionFailed; + public static Action? OnP2PConnectionFailed; /// /// This should be called in response to a OnP2PSessionRequest /// - public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal.AcceptP2PSessionWithUser( user ); + public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal != null && Internal.AcceptP2PSessionWithUser( user ); /// /// Allow or disallow P2P connects to fall back on Steam server relay if direct /// connection or NAT traversal can't be established. Applies to connections /// created after setting or old connections that need to reconnect. /// - public static bool AllowP2PPacketRelay( bool allow ) => Internal.AllowP2PPacketRelay( allow ); + public static bool AllowP2PPacketRelay( bool allow ) => Internal != null && Internal.AllowP2PPacketRelay( allow ); /// /// This should be called when you're done communicating with a user, as this will @@ -62,7 +62,7 @@ namespace Steamworks /// If the remote user tries to send data to you again, a new OnP2PSessionRequest /// callback will be posted /// - public static bool CloseP2PSessionWithUser( SteamId user ) => Internal.CloseP2PSessionWithUser( user ); + public static bool CloseP2PSessionWithUser( SteamId user ) => Internal != null && Internal.CloseP2PSessionWithUser( user ); /// /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. @@ -70,7 +70,7 @@ namespace Steamworks public static bool IsP2PPacketAvailable( int channel = 0 ) { uint _ = 0; - return Internal.IsP2PPacketAvailable( ref _, channel ); + return Internal != null && Internal.IsP2PPacketAvailable( ref _, channel ); } /// @@ -80,7 +80,7 @@ namespace Steamworks { uint size = 0; - if ( !Internal.IsP2PPacketAvailable( ref size, channel ) ) + if ( Internal is null || !Internal.IsP2PPacketAvailable( ref size, channel ) ) return null; var buffer = Helpers.TakeBuffer( (int) size ); @@ -108,7 +108,7 @@ namespace Steamworks public unsafe static bool ReadP2PPacket( byte[] buffer, ref uint size, ref SteamId steamid, int channel = 0 ) { fixed (byte* p = buffer) { - return Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); } } @@ -117,7 +117,7 @@ namespace Steamworks /// public unsafe static bool ReadP2PPacket( byte* buffer, uint cbuf, ref uint size, ref SteamId steamid, int channel = 0 ) { - return Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); } /// @@ -132,7 +132,7 @@ namespace Steamworks fixed ( byte* p = data ) { - return Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); } } @@ -143,13 +143,13 @@ namespace Steamworks /// public static unsafe bool SendP2PPacket( SteamId steamid, byte* data, uint length, int nChannel = 1, P2PSend sendType = P2PSend.Reliable ) { - return Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); } public static P2PSessionState? GetP2PSessionState( SteamId steamid ) { P2PSessionState_t state = new P2PSessionState_t(); - if (Internal.GetP2PSessionState(steamid, ref state)) + if (Internal != null && Internal.GetP2PSessionState(steamid, ref state)) { return new P2PSessionState(state); } diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs index ea5e983ac..50e80dfd2 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworkingSockets : SteamSharedClass { - internal static ISteamNetworkingSockets Internal => Interface as ISteamNetworkingSockets; + internal static ISteamNetworkingSockets? Internal => Interface as ISteamNetworkingSockets; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks static readonly Dictionary SocketInterfaces = new Dictionary(); - internal static SocketManager GetSocketManager( uint id ) + internal static SocketManager? GetSocketManager( uint id ) { if ( SocketInterfaces == null ) return null; if ( id == 0 ) throw new System.ArgumentException( "Invalid Socket" ); @@ -43,7 +43,7 @@ namespace Steamworks #region ConnectionInterface static readonly Dictionary ConnectionInterfaces = new Dictionary(); - internal static ConnectionManager GetConnectionManager( uint id ) + internal static ConnectionManager? GetConnectionManager( uint id ) { if ( ConnectionInterfaces == null ) return null; if ( id == 0 ) return null; @@ -88,7 +88,7 @@ namespace Steamworks OnConnectionStatusChanged?.Invoke( data.Conn, data.Nfo ); } - public static event Action OnConnectionStatusChanged; + public static event Action? OnConnectionStatusChanged; /// @@ -98,8 +98,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateNormalSocket( NetAddress address ) where T : SocketManager, new() + public static T? CreateNormalSocket( NetAddress address ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -118,8 +120,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateNormalSocket( NetAddress address, ISocketManager intrface ) + public static SocketManager? CreateNormalSocket( NetAddress address, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -138,8 +142,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static T ConnectNormal( NetAddress address ) where T : ConnectionManager, new() + public static T? ConnectNormal( NetAddress address ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -150,8 +156,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static ConnectionManager ConnectNormal( NetAddress address, IConnectionManager iface ) + public static ConnectionManager? ConnectNormal( NetAddress address, IConnectionManager iface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -171,8 +179,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() + public static T? CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -189,8 +199,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateRelaySocket( int virtualport, ISocketManager intrface ) + public static SocketManager? CreateRelaySocket( int virtualport, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -209,8 +221,10 @@ namespace Steamworks /// /// Connect to a relay server /// - public static T ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() + public static T? ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); NetIdentity identity = serverId; var options = Array.Empty(); diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index e78c07bf0..319e5d641 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamNetworkingUtils : SteamSharedClass { - internal static ISteamNetworkingUtils Internal => Interface as ISteamNetworkingUtils; + internal static ISteamNetworkingUtils? Internal => Interface as ISteamNetworkingUtils; internal override void InitializeInterface( bool server ) { @@ -43,7 +43,7 @@ namespace Steamworks /// and your frame rate will tank and you won't know why. /// - public static event Action OnDebugOutput; + public static event Action? OnDebugOutput; public struct SteamRelayNetworkStatus { @@ -82,7 +82,7 @@ namespace Steamworks /// public static void InitRelayNetworkAccess() { - Internal.InitRelayNetworkAccess(); + Internal?.InitRelayNetworkAccess(); } /// @@ -98,6 +98,8 @@ namespace Steamworks { get { + if (Internal is null) { return null; } + NetPingLocation location = default; var age = Internal.GetLocalPingLocation( ref location ); if ( age < 0 ) @@ -114,7 +116,7 @@ namespace Steamworks /// public static int EstimatePingTo( NetPingLocation target ) { - return Internal.EstimatePingTimeFromLocalHost( ref target ); + return Internal?.EstimatePingTimeFromLocalHost( ref target ) ?? 0; } /// @@ -124,7 +126,7 @@ namespace Steamworks public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { await Task.Yield(); - if ( Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) + if ( Internal is null || Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) return; SteamRelayNetworkStatus_t status = default; @@ -135,7 +137,7 @@ namespace Steamworks } } - public static long LocalTimestamp => Internal.GetLocalTimestamp(); + public static long LocalTimestamp => Internal?.GetLocalTimestamp() ?? 0; /// @@ -223,7 +225,7 @@ namespace Steamworks _debugLevel = value; _debugFunc = new NetDebugFunc( OnDebugMessage ); - Internal.SetDebugOutputFunction( value, _debugFunc ); + Internal?.SetDebugOutputFunction( value, _debugFunc ); } } @@ -235,7 +237,7 @@ namespace Steamworks /// /// We need to keep the delegate around until it's not used anymore /// - static NetDebugFunc _debugFunc; + static NetDebugFunc? _debugFunc; struct DebugMessage { @@ -274,7 +276,7 @@ namespace Steamworks internal unsafe static bool SetConfigInt( NetConfig type, int value ) { int* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); } internal unsafe static int GetConfigInt( NetConfig type ) @@ -283,7 +285,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Int32; int* ptr = &value; UIntPtr size = new UIntPtr( sizeof( int ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -293,7 +295,7 @@ namespace Steamworks internal unsafe static bool SetConfigFloat( NetConfig type, float value ) { float* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); } internal unsafe static float GetConfigFloat( NetConfig type ) @@ -302,7 +304,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Float; float* ptr = &value; UIntPtr size = new UIntPtr( sizeof( float ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -315,7 +317,7 @@ namespace Steamworks fixed ( byte* ptr = bytes ) { - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamParental.cs b/Libraries/Facepunch.Steamworks/SteamParental.cs index b746ca3b6..7f3a23bd6 100644 --- a/Libraries/Facepunch.Steamworks/SteamParental.cs +++ b/Libraries/Facepunch.Steamworks/SteamParental.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamParental : SteamSharedClass { - internal static ISteamParentalSettings Internal => Interface as ISteamParentalSettings; + internal static ISteamParentalSettings? Internal => Interface as ISteamParentalSettings; internal override void InitializeInterface( bool server ) { @@ -28,37 +28,37 @@ namespace Steamworks /// /// Parental Settings Changed /// - public static event Action OnSettingsChanged; + public static event Action? OnSettingsChanged; /// /// /// - public static bool IsParentalLockEnabled => Internal.BIsParentalLockEnabled(); + public static bool IsParentalLockEnabled => Internal != null && Internal.BIsParentalLockEnabled(); /// /// /// - public static bool IsParentalLockLocked => Internal.BIsParentalLockLocked(); + public static bool IsParentalLockLocked => Internal != null && Internal.BIsParentalLockLocked(); /// /// /// - public static bool IsAppBlocked( AppId app ) => Internal.BIsAppBlocked( app.Value ); + public static bool IsAppBlocked( AppId app ) => Internal != null && Internal.BIsAppBlocked( app.Value ); /// /// /// - public static bool BIsAppInBlockList( AppId app ) => Internal.BIsAppInBlockList( app.Value ); + public static bool BIsAppInBlockList( AppId app ) => Internal != null && Internal.BIsAppInBlockList( app.Value ); /// /// /// - public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal.BIsFeatureBlocked( feature ); + public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureBlocked( feature ); /// /// /// - public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal.BIsFeatureInBlockList( feature ); + public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureInBlockList( feature ); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamParties.cs b/Libraries/Facepunch.Steamworks/SteamParties.cs index aef57bb69..1d7c1eba3 100644 --- a/Libraries/Facepunch.Steamworks/SteamParties.cs +++ b/Libraries/Facepunch.Steamworks/SteamParties.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamParties : SteamClientClass { - internal static ISteamParties Internal => Interface as ISteamParties; + internal static ISteamParties? Internal => Interface as ISteamParties; internal override void InitializeInterface( bool server ) { @@ -32,15 +32,15 @@ namespace Steamworks /// /// The list of possible Party beacon locations has changed /// - public static event Action OnBeaconLocationsUpdated; + public static event Action? OnBeaconLocationsUpdated; /// /// The list of active beacons may have changed /// - public static event Action OnActiveBeaconsUpdated; + public static event Action? OnActiveBeaconsUpdated; - public static int ActiveBeaconCount => (int) Internal.GetNumActiveBeacons(); + public static int ActiveBeaconCount => (int)(Internal?.GetNumActiveBeacons() ?? 0); public static IEnumerable ActiveBeacons { @@ -50,7 +50,7 @@ namespace Steamworks { yield return new PartyBeacon { - Id = Internal.GetBeaconByIndex( i ) + Id = Internal?.GetBeaconByIndex( i ) ?? 0 }; } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs index 48a1945d2..502d37291 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemotePlay : SteamClientClass { - internal static ISteamRemotePlay Internal => Interface as ISteamRemotePlay; + internal static ISteamRemotePlay? Internal => Interface as ISteamRemotePlay; internal override void InitializeInterface( bool server ) { @@ -30,29 +30,29 @@ namespace Steamworks /// /// Called when a session is connected /// - public static event Action OnSessionConnected; + public static event Action? OnSessionConnected; /// /// Called when a session becomes disconnected /// - public static event Action OnSessionDisconnected; + public static event Action? OnSessionDisconnected; /// /// Get the number of currently connected Steam Remote Play sessions /// - public static int SessionCount => (int) Internal.GetSessionCount(); + public static int SessionCount => (int)(Internal?.GetSessionCount() ?? 0); /// /// Get the currently connected Steam Remote Play session ID at the specified index. /// IsValid will return false if it's out of bounds /// - public static RemotePlaySession GetSession( int index ) => (RemotePlaySession) Internal.GetSessionID( index ).Value; + public static RemotePlaySession GetSession( int index ) => Internal?.GetSessionID( index ).Value ?? default; /// /// Invite a friend to Remote Play Together /// This returns false if the invite can't be sent /// - public static bool SendInvite( SteamId steamid ) => Internal.BSendRemotePlayTogetherInvite( steamid ); + public static bool SendInvite( SteamId steamid ) => Internal != null && Internal.BSendRemotePlayTogetherInvite( steamid ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs index bfb9a0a6b..847147680 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemoteStorage : SteamClientClass { - internal static ISteamRemoteStorage Internal => Interface as ISteamRemoteStorage; + internal static ISteamRemoteStorage? Internal => Interface as ISteamRemoteStorage; internal override void InitializeInterface( bool server ) { @@ -28,15 +28,17 @@ namespace Steamworks { fixed ( byte* ptr = data ) { - return Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); + return Internal != null && Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); } } /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. /// - public unsafe static byte[] FileRead( string filename ) + public unsafe static byte[]? FileRead( string filename ) { + if (Internal is null) { return null; } + var size = FileSize( filename ); if ( size <= 0 ) return null; var buffer = new byte[size]; @@ -51,32 +53,32 @@ namespace Steamworks /// /// Checks whether the specified file exists. /// - public static bool FileExists( string filename ) => Internal.FileExists( filename ); + public static bool FileExists( string filename ) => Internal != null && Internal.FileExists( filename ); /// /// Checks if a specific file is persisted in the steam cloud. /// - public static bool FilePersisted( string filename ) => Internal.FilePersisted( filename ); + public static bool FilePersisted( string filename ) => Internal != null && Internal.FilePersisted( filename ); /// /// Gets the specified file's last modified date/time. /// - public static DateTime FileTime( string filename ) => Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ); + public static DateTime FileTime( string filename ) => Internal != null ? Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ) : default; /// /// Gets the specified files size in bytes. 0 if not exists. /// - public static int FileSize( string filename ) => Internal.GetFileSize( filename ); + public static int FileSize( string filename ) => Internal?.GetFileSize( filename ) ?? 0; /// /// Deletes the file from remote storage, but leaves it on the local disk and remains accessible from the API. /// - public static bool FileForget( string filename ) => Internal.FileForget( filename ); + public static bool FileForget( string filename ) => Internal != null && Internal.FileForget( filename ); /// /// Deletes a file from the local disk, and propagates that delete to the cloud. /// - public static bool FileDelete( string filename ) => Internal.FileDelete( filename ); + public static bool FileDelete( string filename ) => Internal != null && Internal.FileDelete( filename ); /// @@ -87,7 +89,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t; } } @@ -100,7 +102,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t - a; } } @@ -113,7 +115,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return a; } } @@ -127,7 +129,7 @@ namespace Steamworks /// Checks if the account wide Steam Cloud setting is enabled for this user /// or if they disabled it in the Settings->Cloud dialog. /// - public static bool IsCloudEnabledForAccount => Internal.IsCloudEnabledForAccount(); + public static bool IsCloudEnabledForAccount => Internal != null && Internal.IsCloudEnabledForAccount(); /// /// Checks if the per game Steam Cloud setting is enabled for this user @@ -139,14 +141,14 @@ namespace Steamworks /// public static bool IsCloudEnabledForApp { - get => Internal.IsCloudEnabledForApp(); - set => Internal.SetCloudEnabledForApp( value ); + get => Internal != null && Internal.IsCloudEnabledForApp(); + set => Internal?.SetCloudEnabledForApp( value ); } /// /// Gets the total number of local files synchronized by Steam Cloud. /// - public static int FileCount => Internal.GetFileCount(); + public static int FileCount => Internal?.GetFileCount() ?? 0; public struct RemoteFile { @@ -155,7 +157,7 @@ namespace Steamworks public bool Delete() { - return Internal.FileDelete(Filename); + return Internal != null && Internal.FileDelete(Filename); } } @@ -167,6 +169,8 @@ namespace Steamworks get { var ret = new List(); + if (Internal is null) { return ret; } + int count = FileCount; for( int i=0; i public class SteamScreenshots : SteamClientClass { - internal static ISteamScreenshots Internal => Interface as ISteamScreenshots; + internal static ISteamScreenshots? Internal => Interface as ISteamScreenshots; internal override void InitializeInterface( bool server ) { @@ -37,17 +37,17 @@ namespace Steamworks /// This will only be called if Hooked is true, in which case Steam /// will not take the screenshot itself. /// - public static event Action OnScreenshotRequested; + public static event Action? OnScreenshotRequested; /// /// A screenshot successfully written or otherwise added to the library and can now be tagged. /// - public static event Action OnScreenshotReady; + public static event Action? OnScreenshotReady; /// /// A screenshot attempt failed /// - public static event Action OnScreenshotFailed; + public static event Action? OnScreenshotFailed; /// /// Writes a screenshot to the user's screenshot library given the raw image data, which must be in RGB format. @@ -55,6 +55,8 @@ namespace Steamworks /// public unsafe static Screenshot? WriteScreenshot( byte[] data, int width, int height ) { + if (Internal is null) { return null; } + fixed ( byte* ptr = data ) { var handle = Internal.WriteScreenshot( (IntPtr)ptr, (uint)data.Length, width, height ); @@ -72,6 +74,8 @@ namespace Steamworks /// public unsafe static Screenshot? AddScreenshot( string filename, string thumbnail, int width, int height ) { + if (Internal is null) { return null; } + var handle = Internal.AddScreenshotToLibrary( filename, thumbnail, width, height ); if ( handle.Value == 0 ) return null; @@ -83,7 +87,7 @@ namespace Steamworks /// If screenshots are being hooked by the game then a /// ScreenshotRequested callback is sent back to the game instead. /// - public static void TriggerScreenshot() => Internal.TriggerScreenshot(); + public static void TriggerScreenshot() => Internal?.TriggerScreenshot(); /// /// Toggles whether the overlay handles screenshots when the user presses the screenshot hotkey, or if the game handles them. @@ -93,8 +97,8 @@ namespace Steamworks /// public static bool Hooked { - get => Internal.IsScreenshotsHooked(); - set => Internal.HookScreenshots( value ); + get => Internal != null && Internal.IsScreenshotsHooked(); + set => Internal?.HookScreenshots( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index 1b1559d45..b3ea1bef5 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Steamworks.Data; +using System.Diagnostics.CodeAnalysis; namespace Steamworks { @@ -12,7 +13,7 @@ namespace Steamworks /// public partial class SteamServer : SteamServerClass { - internal static ISteamGameServer Internal => Interface as ISteamGameServer; + internal static ISteamGameServer? Internal => Interface as ISteamGameServer; internal override void InitializeInterface( bool server ) { @@ -34,28 +35,28 @@ namespace Steamworks /// /// User has been authed or rejected /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Called when a connections to the Steam back-end has been established. /// This means the server now is logged on and has a working connection to the Steam master server. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying) /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Disconnected from Steam /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Called when authentication status changes, useful for grabbing SteamId once aavailability is current /// - public static event Action OnSteamNetAuthenticationStatus; + public static event Action? OnSteamNetAuthenticationStatus; /// @@ -172,7 +173,7 @@ namespace Steamworks public static bool DedicatedServer { get => _dedicatedServer; - set { if ( _dedicatedServer == value ) return; Internal.SetDedicatedServer( value ); _dedicatedServer = value; } + set { if ( _dedicatedServer == value ) return; Internal?.SetDedicatedServer( value ); _dedicatedServer = value; } } private static bool _dedicatedServer; @@ -183,7 +184,7 @@ namespace Steamworks public static int MaxPlayers { get => _maxplayers; - set { if ( _maxplayers == value ) return; Internal.SetMaxPlayerCount( value ); _maxplayers = value; } + set { if ( _maxplayers == value ) return; Internal?.SetMaxPlayerCount( value ); _maxplayers = value; } } private static int _maxplayers = 0; @@ -194,7 +195,7 @@ namespace Steamworks public static int BotCount { get => _botcount; - set { if ( _botcount == value ) return; Internal.SetBotPlayerCount( value ); _botcount = value; } + set { if ( _botcount == value ) return; Internal?.SetBotPlayerCount( value ); _botcount = value; } } private static int _botcount = 0; @@ -204,9 +205,9 @@ namespace Steamworks public static string MapName { get => _mapname; - set { if ( _mapname == value ) return; Internal.SetMapName( value ); _mapname = value; } + set { if ( _mapname == value ) return; Internal?.SetMapName( value ); _mapname = value; } } - private static string _mapname; + private static string _mapname = ""; /// /// Gets or sets the current ModDir @@ -214,7 +215,7 @@ namespace Steamworks public static string ModDir { get => _modDir; - internal set { if ( _modDir == value ) return; Internal.SetModDir( value ); _modDir = value; } + internal set { if ( _modDir == value ) return; Internal?.SetModDir( value ); _modDir = value; } } private static string _modDir = ""; @@ -224,7 +225,7 @@ namespace Steamworks public static string Product { get => _product; - internal set { if ( _product == value ) return; Internal.SetProduct( value ); _product = value; } + internal set { if ( _product == value ) return; Internal?.SetProduct( value ); _product = value; } } private static string _product = ""; @@ -234,7 +235,7 @@ namespace Steamworks public static string GameDescription { get => _gameDescription; - internal set { if ( _gameDescription == value ) return; Internal.SetGameDescription( value ); _gameDescription = value; } + internal set { if ( _gameDescription == value ) return; Internal?.SetGameDescription( value ); _gameDescription = value; } } private static string _gameDescription = ""; @@ -244,7 +245,7 @@ namespace Steamworks public static string ServerName { get => _serverName; - set { if ( _serverName == value ) return; Internal.SetServerName( value ); _serverName = value; } + set { if ( _serverName == value ) return; Internal?.SetServerName( value ); _serverName = value; } } private static string _serverName = ""; @@ -254,7 +255,7 @@ namespace Steamworks public static bool Passworded { get => _passworded; - set { if ( _passworded == value ) return; Internal.SetPasswordProtected( value ); _passworded = value; } + set { if ( _passworded == value ) return; Internal?.SetPasswordProtected( value ); _passworded = value; } } private static bool _passworded; @@ -268,20 +269,20 @@ namespace Steamworks set { if ( _gametags == value ) return; - Internal.SetGameTags( value ); + Internal?.SetGameTags( value ); _gametags = value; } } private static string _gametags = ""; - public static SteamId SteamId => Internal.GetSteamID(); + public static SteamId SteamId => Internal?.GetSteamID() ?? default; /// /// Log onto Steam anonymously. /// public static void LogOnAnonymous() { - Internal.LogOnAnonymous(); + Internal?.LogOnAnonymous(); ForceHeartbeat(); } @@ -290,21 +291,21 @@ namespace Steamworks /// public static void LogOff() { - Internal.LogOff(); + Internal?.LogOff(); } /// /// Returns true if the server is connected and registered with the Steam master server /// You should have called LogOnAnonymous etc on startup. /// - public static bool LoggedOn => Internal.BLoggedOn(); + public static bool LoggedOn => Internal != null && Internal.BLoggedOn(); /// /// To the best of its ability this tries to get the server's /// current public ip address. Be aware that this is likely to return /// null for the first few seconds after initialization. /// - public static System.Net.IPAddress PublicIp => Internal.GetPublicIP(); + public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP(); /// /// Enable or disable heartbeats, which are sent regularly to the master server. @@ -312,7 +313,7 @@ namespace Steamworks /// public static bool AutomaticHeartbeats { - set { Internal.EnableHeartbeats( value ); } + set { Internal?.EnableHeartbeats( value ); } } /// @@ -321,7 +322,7 @@ namespace Steamworks /// public static int AutomaticHeartbeatRate { - set { Internal.SetHeartbeatInterval( value ); } + set { Internal?.SetHeartbeatInterval( value ); } } /// @@ -330,7 +331,7 @@ namespace Steamworks /// public static void ForceHeartbeat() { - Internal.ForceHeartbeat(); + Internal?.ForceHeartbeat(); } /// @@ -340,7 +341,7 @@ namespace Steamworks /// public static void UpdatePlayer( SteamId steamid, string name, int score ) { - Internal.BUpdateUserData( steamid, name, (uint)score ); + Internal?.BUpdateUserData( steamid, name, (uint)score ); } static Dictionary KeyValue = new Dictionary(); @@ -353,6 +354,8 @@ namespace Steamworks /// public static void SetKey( string Key, string Value ) { + if (Internal is null) { return; } + if ( KeyValue.ContainsKey( Key ) ) { if ( KeyValue[Key] == Value ) @@ -374,7 +377,7 @@ namespace Steamworks public static void ClearKeys() { KeyValue.Clear(); - Internal.ClearAllKeyValues(); + Internal?.ClearAllKeyValues(); } /// @@ -382,6 +385,7 @@ namespace Steamworks /// public static unsafe BeginAuthResult BeginAuthSession( byte[] data, SteamId steamid ) { + if (Internal is null) { return BeginAuthResult.ServerNotConnectedToSteam; } fixed ( byte* p = data ) { var result = Internal.BeginAuthSession( (IntPtr)p, data.Length, steamid ); @@ -395,7 +399,7 @@ namespace Steamworks /// public static void EndSession( SteamId steamid ) { - Internal.EndAuthSession( steamid ); + Internal?.EndAuthSession( steamid ); } /// @@ -406,6 +410,12 @@ namespace Steamworks /// True if we want to send a packet public static unsafe bool GetOutgoingPacket( out OutgoingPacket packet ) { + if (Internal is null) + { + packet = default; + return false; + } + var buffer = Helpers.TakeBuffer( 1024 * 32 ); packet = new OutgoingPacket(); @@ -442,7 +452,7 @@ namespace Steamworks /// public static unsafe void HandleIncomingPacket( IntPtr ptr, int size, uint address, ushort port ) { - Internal.HandleIncomingPacket( ptr, size, address, port ); + Internal?.HandleIncomingPacket( ptr, size, address, port ); } /// @@ -450,7 +460,7 @@ namespace Steamworks /// public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid ) { - return Internal.UserHasLicenseForApp( steamid, appid ); + return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServerStats.cs b/Libraries/Facepunch.Steamworks/SteamServerStats.cs index 2de7b0f14..823a92771 100644 --- a/Libraries/Facepunch.Steamworks/SteamServerStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamServerStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamServerStats : SteamServerClass { - internal static ISteamGameServerStats Internal => Interface as ISteamGameServerStats; + internal static ISteamGameServerStats? Internal => Interface as ISteamGameServerStats; internal override void InitializeInterface( bool server ) { @@ -24,6 +24,7 @@ namespace Steamworks /// public static async Task RequestUserStatsAsync( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.RequestUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; @@ -35,7 +36,7 @@ namespace Steamworks /// public static bool SetInt( SteamId steamid, string name, int stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -44,7 +45,7 @@ namespace Steamworks /// public static bool SetFloat( SteamId steamid, string name, float stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -56,7 +57,7 @@ namespace Steamworks { int data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -71,7 +72,7 @@ namespace Steamworks { float data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -83,7 +84,7 @@ namespace Steamworks /// public static bool SetAchievement( SteamId steamid, string name ) { - return Internal.SetUserAchievement( steamid, name ); + return Internal != null && Internal.SetUserAchievement( steamid, name ); } /// @@ -92,7 +93,7 @@ namespace Steamworks /// public static bool ClearAchievement( SteamId steamid, string name ) { - return Internal.ClearUserAchievement( steamid, name ); + return Internal != null && Internal.ClearUserAchievement( steamid, name ); } /// @@ -102,7 +103,7 @@ namespace Steamworks { bool achieved = false; - if ( !Internal.GetUserAchievement( steamid, name, ref achieved ) ) + if ( Internal is null || !Internal.GetUserAchievement( steamid, name, ref achieved ) ) return false; return achieved; @@ -115,6 +116,7 @@ namespace Steamworks /// public static async Task StoreUserStats( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.StoreUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 21c51a991..ecf20286e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUGC : SteamSharedClass { - internal static ISteamUGC Internal => Interface as ISteamUGC; + internal static ISteamUGC? Internal => Interface as ISteamUGC; internal override void InitializeInterface( bool server ) { @@ -44,10 +44,11 @@ namespace Steamworks /// /// Posted after Download call /// - public static event Action OnDownloadItemResult; + public static event Action? OnDownloadItemResult; public static async Task DeleteFileAsync( PublishedFileId fileId ) { + if (Internal is null) { return false; } var r = await Internal.DeleteItem( fileId ); return r?.Result == Result.OK; } @@ -60,7 +61,7 @@ namespace Steamworks /// true if nothing went wrong and the download is started public static bool Download( PublishedFileId fileId, bool highPriority = false ) { - return Internal.DownloadItem( fileId, highPriority ); + return Internal != null && Internal.DownloadItem( fileId, highPriority ); } /// @@ -73,7 +74,7 @@ namespace Steamworks /// true if downloaded and installed correctly public static async Task DownloadAsync( PublishedFileId fileId, - Action progress = null, + Action? progress = null, int millisecondsUpdateDelay = 60, CancellationToken? ct = null) { @@ -163,28 +164,32 @@ namespace Steamworks public static async Task StartPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StartPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTrackingForAllItems() { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTrackingForAllItems(); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } - public static Action GlobalOnItemInstalled; + public static Action? GlobalOnItemInstalled; - public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } + public static uint NumSubscribedItems { get { return Internal?.GetNumSubscribedItems() ?? 0; } } public static PublishedFileId[] GetSubscribedItems() { + if (Internal is null) { return Array.Empty(); } uint numSubscribed = NumSubscribedItems; PublishedFileId[] ids = new PublishedFileId[numSubscribed]; Internal.GetSubscribedItems(ids, numSubscribed); diff --git a/Libraries/Facepunch.Steamworks/SteamUser.cs b/Libraries/Facepunch.Steamworks/SteamUser.cs index e481d8493..8f4c6620e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUser.cs +++ b/Libraries/Facepunch.Steamworks/SteamUser.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUser : SteamClientClass { - internal static ISteamUser Internal => Interface as ISteamUser; + internal static ISteamUser? Internal => Interface as ISteamUser; internal override void InitializeInterface( bool server ) { @@ -26,7 +26,7 @@ namespace Steamworks SampleRate = OptimalSampleRate; } - static Dictionary richPresence; + static Dictionary? richPresence; internal static void InstallEvents() { @@ -48,20 +48,20 @@ namespace Steamworks /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// Called when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Called if the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Sent by the Steam server to the client telling it to disconnect from the specified game server, @@ -69,12 +69,12 @@ namespace Steamworks /// The game client should immediately disconnect upon receiving this message. /// This can usually occur if the user doesn't have rights to play on the game server. /// - public static event Action OnClientGameServerDeny; + public static event Action? OnClientGameServerDeny; /// /// Called whenever the users licenses (owned packages) changes. /// - public static event Action OnLicensesUpdated; + public static event Action? OnLicensesUpdated; /// /// Called when an auth ticket has been validated. @@ -82,18 +82,18 @@ namespace Steamworks /// The second is the Steam ID that owns the game, this will be different from the first /// if the game is being borrowed via Steam Family Sharing /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Used internally for GetAuthSessionTicketAsync /// - internal static event Action OnGetAuthSessionTicketResponse; + internal static event Action? OnGetAuthSessionTicketResponse; /// /// Called when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// - public static event Action OnMicroTxnAuthorizationResponse; + public static event Action? OnMicroTxnAuthorizationResponse; /// /// Sent to your game in response to a steam://gamewebcallback/(appid)/command/stuff command from a user clicking a @@ -101,14 +101,14 @@ namespace Steamworks /// You can use this to add support for external site signups where you want to pop back into the browser after some web page /// signup sequence, and optionally get back some detail about that. /// - public static event Action OnGameWebCallback; + public static event Action? OnGameWebCallback; /// /// Sent for games with enabled anti indulgence / duration control, for enabled users. /// Lets the game know whether persistent rewards or XP should be granted at normal rate, /// half rate, or zero rate. /// - public static event Action OnDurationControl; + public static event Action? OnDurationControl; @@ -127,8 +127,8 @@ namespace Steamworks set { _recordingVoice = value; - if ( value ) Internal.StartVoiceRecording(); - else Internal.StopVoiceRecording(); + if ( value ) Internal?.StartVoiceRecording(); + else Internal?.StopVoiceRecording(); } } @@ -142,7 +142,7 @@ namespace Steamworks { uint szCompressed = 0, deprecated = 0; - if ( Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) return false; return szCompressed > 0; @@ -168,7 +168,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return 0; } @@ -185,7 +185,7 @@ namespace Steamworks /// ReadVoiceData because it won't be creating a new byte array every call. But this /// makes it easier to get it working, so let the babies have their bottle. /// - public static unsafe byte[] ReadVoiceDataBytes() + public static unsafe byte[]? ReadVoiceDataBytes() { if ( !HasVoiceData ) return null; @@ -195,7 +195,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return null; } @@ -222,7 +222,7 @@ namespace Steamworks } } - public static uint OptimalSampleRate => Internal.GetVoiceOptimalSampleRate(); + public static uint OptimalSampleRate => Internal?.GetVoiceOptimalSampleRate() ?? 0; /// @@ -247,7 +247,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -273,7 +273,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -297,7 +297,7 @@ namespace Steamworks uint szWritten = 0; - if ( Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; return (int)szWritten; @@ -306,14 +306,14 @@ namespace Steamworks /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// - public static unsafe AuthTicket GetAuthSessionTicket() + public static unsafe AuthTicket? GetAuthSessionTicket() { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; - uint ticket = Internal.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ); + uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ) ?? 0; if ( ticket == 0 ) return null; @@ -332,15 +332,15 @@ namespace Steamworks /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback /// times out or returns negatively. /// - public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) + public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) { var result = Result.Pending; - AuthTicket ticket = null; + AuthTicket? ticket = null; var stopwatch = Stopwatch.StartNew(); void f( GetAuthSessionTicketResponse_t t ) { - if ( t.AuthTicket != ticket.Handle ) return; + if ( t.AuthTicket != ticket?.Handle ) return; result = t.Result; } @@ -379,11 +379,11 @@ namespace Steamworks { fixed ( byte* ptr = ticketData ) { - return Internal.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ); + return Internal?.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ) ?? BeginAuthResult.ServerNotConnectedToSteam; } } - public static void EndAuthSession( SteamId steamid ) => Internal.EndAuthSession( steamid ); + public static void EndAuthSession( SteamId steamid ) => Internal?.EndAuthSession( steamid ); // UserHasLicenseForApp - SERVER VERSION ( DLC CHECKING ) @@ -392,12 +392,12 @@ namespace Steamworks /// Checks if the current users looks like they are behind a NAT device. /// This is only valid if the user is connected to the Steam servers and may not catch all forms of NAT. /// - public static bool IsBehindNAT => Internal.BIsBehindNAT(); + public static bool IsBehindNAT => Internal != null && Internal.BIsBehindNAT(); /// /// Gets the Steam level of the user, as shown on their Steam community profile. /// - public static int SteamLevel => Internal.GetPlayerSteamLevel(); + public static int SteamLevel => Internal?.GetPlayerSteamLevel() ?? 0; /// /// Requests a URL which authenticates an in-game browser for store check-out, and then redirects to the specified URL. @@ -405,8 +405,10 @@ namespace Steamworks /// NOTE: The URL has a very short lifetime to prevent history-snooping attacks, so you should only call this API when you are about to launch the browser, or else immediately navigate to the result URL using a hidden browser window. /// NOTE: The resulting authorization cookie has an expiration time of one day, so it would be a good idea to request and visit a new auth URL every 12 hours. /// - public static async Task GetStoreAuthUrlAsync( string url ) + public static async Task GetStoreAuthUrlAsync( string url ) { + if (Internal is null) { return null; } + var response = await Internal.RequestStoreAuthURL( url ); if ( !response.HasValue ) return null; @@ -417,22 +419,22 @@ namespace Steamworks /// /// Checks whether the current user has verified their phone number. /// - public static bool IsPhoneVerified => Internal.BIsPhoneVerified(); + public static bool IsPhoneVerified => Internal != null && Internal.BIsPhoneVerified(); /// /// Checks whether the current user has Steam Guard two factor authentication enabled on their account. /// - public static bool IsTwoFactorEnabled => Internal.BIsTwoFactorEnabled(); + public static bool IsTwoFactorEnabled => Internal != null && Internal.BIsTwoFactorEnabled(); /// /// Checks whether the user's phone number is used to uniquely identify them. /// - public static bool IsPhoneIdentifying => Internal.BIsPhoneIdentifying(); + public static bool IsPhoneIdentifying => Internal != null && Internal.BIsPhoneIdentifying(); /// /// Checks whether the current user's phone number is awaiting (re)verification. /// - public static bool IsPhoneRequiringVerification => Internal.BIsPhoneRequiringVerification(); + public static bool IsPhoneRequiringVerification => Internal != null && Internal.BIsPhoneRequiringVerification(); /// /// Requests an application ticket encrypted with the secret "encrypted app ticket key". @@ -441,8 +443,10 @@ namespace Steamworks /// If you get a null result from this it's probably because you're calling it too often. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) + public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) { + if (Internal is null) { return null; } + var dataPtr = Marshal.AllocHGlobal( dataToInclude.Length ); Marshal.Copy( dataToInclude, 0, dataPtr, dataToInclude.Length ); @@ -453,7 +457,7 @@ namespace Steamworks var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -477,14 +481,16 @@ namespace Steamworks /// There can only be one call pending, and this call is subject to a 60 second rate limit. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync() + public static async Task RequestEncryptedAppTicketAsync() { + if (Internal is null) { return null; } + var result = await Internal.RequestEncryptedAppTicket( IntPtr.Zero, 0 ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -504,6 +510,8 @@ namespace Steamworks /// public static async Task GetDurationControl() { + if (Internal is null) { return default; } + var response = await Internal.GetDurationControl(); if ( !response.HasValue ) return default; diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index 97bbb31f0..f9fc6fa5d 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamUserStats : SteamClientClass { - internal static ISteamUserStats Internal => Interface as ISteamUserStats; + internal static ISteamUserStats? Internal => Interface as ISteamUserStats; internal override void InitializeInterface( bool server ) { @@ -40,31 +40,31 @@ namespace Steamworks /// /// called when the achivement icon is loaded /// - internal static event Action OnAchievementIconFetched; + internal static event Action? OnAchievementIconFetched; /// /// called when the latests stats and achievements have been received /// from the server /// - public static event Action OnUserStatsReceived; + public static event Action? OnUserStatsReceived; /// /// result of a request to store the user stats for a game /// - public static event Action OnUserStatsStored; + public static event Action? OnUserStatsStored; /// /// result of a request to store the achievements for a game, or an /// "indicate progress" call. If both m_nCurProgress and m_nMaxProgress /// are zero, that means the achievement has been fully unlocked /// - public static event Action OnAchievementProgress; + public static event Action? OnAchievementProgress; /// /// Callback indicating that a user's stats have been unloaded /// - public static event Action OnUserStatsUnloaded; + public static event Action? OnUserStatsUnloaded; /// /// Get the available achievements @@ -73,8 +73,11 @@ namespace Steamworks { get { - for( int i=0; i< Internal.GetNumAchievements(); i++ ) + if (Internal is null) { yield break; } + uint numAchievements = Internal.GetNumAchievements(); + for( int i=0; i < numAchievements; i++ ) { + if (Internal is null) { yield break; } yield return new Achievement( Internal.GetAchievementName( (uint) i ) ); } } @@ -88,6 +91,8 @@ namespace Steamworks /// public static bool IndicateAchievementProgress( string achName, int curProg, int maxProg ) { + if (Internal is null) { return false; } + if ( string.IsNullOrEmpty( achName ) ) throw new ArgumentNullException( "Achievement string is null or empty" ); @@ -103,6 +108,8 @@ namespace Steamworks /// public static async Task PlayerCountAsync() { + if (Internal is null) { return -1; } + var result = await Internal.GetNumberOfCurrentPlayers(); if ( !result.HasValue || result.Value.Success == 0 ) return -1; @@ -122,7 +129,7 @@ namespace Steamworks /// public static bool StoreStats() { - return Internal.StoreStats(); + return Internal != null && Internal.StoreStats(); } /// @@ -133,7 +140,7 @@ namespace Steamworks /// public static bool RequestCurrentStats() { - return Internal.RequestCurrentStats(); + return Internal != null && Internal.RequestCurrentStats(); } /// @@ -146,6 +153,7 @@ namespace Steamworks /// OK indicates success, InvalidState means you need to call RequestCurrentStats first, Fail means the remote call failed public static async Task RequestGlobalStatsAsync( int days ) { + if (Internal is null) { return Result.Fail; } var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( !result.HasValue ) return Result.Fail; return result.Value.Result; @@ -162,6 +170,7 @@ namespace Steamworks /// public static async Task FindOrCreateLeaderboardAsync( string name, LeaderboardSort sort, LeaderboardDisplay display ) { + if (Internal is null) { return null; } var result = await Internal.FindOrCreateLeaderboard( name, sort, display ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -172,6 +181,7 @@ namespace Steamworks public static async Task FindLeaderboardAsync( string name ) { + if (Internal is null) { return null; } var result = await Internal.FindLeaderboard( name ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -211,7 +221,7 @@ namespace Steamworks /// public static bool SetStat( string name, int value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -220,7 +230,7 @@ namespace Steamworks /// public static bool SetStat( string name, float value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -229,7 +239,7 @@ namespace Steamworks public static int GetStatInt( string name ) { int data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -239,7 +249,7 @@ namespace Steamworks public static float GetStatFloat( string name ) { float data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -250,7 +260,7 @@ namespace Steamworks /// public static bool ResetAll( bool includeAchievements ) { - return Internal.ResetAllStats( includeAchievements ); + return Internal != null && Internal.ResetAllStats( includeAchievements ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamUtils.cs b/Libraries/Facepunch.Steamworks/SteamUtils.cs index 9d3eea0d5..3f1553c23 100644 --- a/Libraries/Facepunch.Steamworks/SteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamUtils : SteamSharedClass { - internal static ISteamUtils Internal => Interface as ISteamUtils; + internal static ISteamUtils? Internal => Interface as ISteamUtils; internal override void InitializeInterface( bool server ) { @@ -38,47 +38,47 @@ namespace Steamworks /// /// The country of the user changed /// - public static event Action OnIpCountryChanged; + public static event Action? OnIpCountryChanged; /// /// Fired when running on a laptop and less than 10 minutes of battery is left, fires then every minute /// The parameter is the number of minutes left /// - public static event Action OnLowBatteryPower; + public static event Action? OnLowBatteryPower; /// /// Called when Steam wants to shutdown /// - public static event Action OnSteamShutdown; + public static event Action? OnSteamShutdown; /// /// Big Picture gamepad text input has been closed. Parameter is true if text was submitted, false if cancelled etc. /// - public static event Action OnGamepadTextInputDismissed; + public static event Action? OnGamepadTextInputDismissed; /// /// Returns the number of seconds since the application was active /// - public static uint SecondsSinceAppActive => Internal.GetSecondsSinceAppActive(); + public static uint SecondsSinceAppActive => Internal?.GetSecondsSinceAppActive() ?? 0; /// /// Returns the number of seconds since the user last moved the mouse etc /// - public static uint SecondsSinceComputerActive => Internal.GetSecondsSinceComputerActive(); + public static uint SecondsSinceComputerActive => Internal?.GetSecondsSinceComputerActive() ?? 0; // the universe this client is connecting to - public static Universe ConnectedUniverse => Internal.GetConnectedUniverse(); + public static Universe ConnectedUniverse => Internal?.GetConnectedUniverse() ?? Universe.Invalid; /// /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) /// - public static DateTime SteamServerTime => Epoch.ToDateTime( Internal.GetServerRealTime() ); + public static DateTime SteamServerTime => Internal != null ? Epoch.ToDateTime( Internal.GetServerRealTime() ) : default; /// /// returns the 2 digit ISO 3166-1-alpha-2 format country code this client is running in (as looked up via an IP-to-location database) /// e.g "US" or "UK". /// - public static string IpCountry => Internal.GetIPCountry(); + public static string? IpCountry => Internal?.GetIPCountry(); /// /// returns true if the image exists, and the buffer was successfully filled out @@ -89,7 +89,7 @@ namespace Steamworks { width = 0; height = 0; - return Internal.GetImageSize( image, ref width, ref height ); + return Internal != null && Internal.GetImageSize( image, ref width, ref height ); } /// @@ -109,7 +109,7 @@ namespace Steamworks var buf = Helpers.TakeBuffer( (int) size ); - if ( !Internal.GetImageRGBA( image, buf, (int)size ) ) + if ( Internal is null || !Internal.GetImageRGBA( image, buf, (int)size ) ) return null; i.Data = new byte[size]; @@ -120,12 +120,12 @@ namespace Steamworks /// /// Returns true if we're using a battery (ie, a laptop not plugged in) /// - public static bool UsingBatteryPower => Internal.GetCurrentBatteryPower() != 255; + public static bool UsingBatteryPower => Internal != null && Internal.GetCurrentBatteryPower() != 255; /// /// Returns battery power [0-1] /// - public static float CurrentBatteryPower => Math.Min( Internal.GetCurrentBatteryPower() / 100, 1.0f ); + public static float CurrentBatteryPower => Math.Min( (Internal?.GetCurrentBatteryPower() ?? 0f) / 100, 1.0f ); static NotificationPosition overlayNotificationPosition = NotificationPosition.BottomRight; @@ -140,7 +140,7 @@ namespace Steamworks set { overlayNotificationPosition = value; - Internal.SetOverlayNotificationPosition( value ); + Internal?.SetOverlayNotificationPosition( value ); } } @@ -148,7 +148,7 @@ namespace Steamworks /// Returns true if the overlay is running and the user can access it. The overlay process could take a few seconds to /// start and hook the game process, so this function will initially return false while the overlay is loading. /// - public static bool IsOverlayEnabled => Internal.IsOverlayEnabled(); + public static bool IsOverlayEnabled => Internal != null && Internal.IsOverlayEnabled(); /// /// Normally this call is unneeded if your game has a constantly running frame loop that calls the @@ -161,7 +161,7 @@ namespace Steamworks /// in that case, and then you can check for this periodically (roughly 33hz is desirable) and make sure you /// refresh the screen with Present or SwapBuffers to allow the overlay to do it's work. /// - public static bool DoesOverlayNeedPresent => Internal.BOverlayNeedsPresent(); + public static bool DoesOverlayNeedPresent => Internal != null && Internal.BOverlayNeedsPresent(); /// /// Asynchronous call to check if an executable file has been signed using the public key set on the signing tab @@ -169,6 +169,8 @@ namespace Steamworks /// public static async Task CheckFileSignatureAsync( string filename ) { + if (Internal is null) { throw new System.Exception( "SteamUtils not initialized" ); } + var r = await Internal.CheckFileSignature( filename ); if ( !r.HasValue ) @@ -184,7 +186,7 @@ namespace Steamworks /// public static bool ShowGamepadTextInput( GamepadTextInputMode inputMode, GamepadTextInputLineMode lineInputMode, string description, int maxChars, string existingText = "" ) { - return Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); + return Internal != null && Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); } /// @@ -192,6 +194,8 @@ namespace Steamworks /// public static string GetEnteredGamepadText() { + if (Internal is null) { return string.Empty; } + var len = Internal.GetEnteredGamepadTextLength(); if ( len == 0 ) return string.Empty; @@ -205,19 +209,19 @@ namespace Steamworks /// returns the language the steam client is running in, you probably want /// Apps.CurrentGameLanguage instead, this is for very special usage cases /// - public static string SteamUILanguage => Internal.GetSteamUILanguage(); + public static string? SteamUILanguage => Internal?.GetSteamUILanguage(); /// /// returns true if Steam itself is running in VR mode /// - public static bool IsSteamRunningInVR => Internal.IsSteamRunningInVR(); + public static bool IsSteamRunningInVR => Internal != null && Internal.IsSteamRunningInVR(); /// /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition /// public static void SetOverlayNotificationInset( int x, int y ) { - Internal.SetOverlayNotificationInset( x, y ); + Internal?.SetOverlayNotificationInset( x, y ); } /// @@ -225,13 +229,13 @@ namespace Steamworks /// Games much be launched through the Steam client to enable the Big Picture overlay. During development, /// a game can be added as a non-steam game to the developers library to test this feature /// - public static bool IsSteamInBigPictureMode => Internal.IsSteamInBigPictureMode(); + public static bool IsSteamInBigPictureMode => Internal != null && Internal.IsSteamInBigPictureMode(); /// /// ask SteamUI to create and render its OpenVR dashboard /// - public static void StartVRDashboard() => Internal.StartVRDashboard(); + public static void StartVRDashboard() => Internal?.StartVRDashboard(); /// /// Set whether the HMD content will be streamed via Steam In-Home Streaming @@ -242,24 +246,24 @@ namespace Steamworks /// public static bool VrHeadsetStreaming { - get => Internal.IsVRHeadsetStreamingEnabled(); + get => Internal != null && Internal.IsVRHeadsetStreamingEnabled(); set { - Internal.SetVRHeadsetStreamingEnabled( value ); + Internal?.SetVRHeadsetStreamingEnabled( value ); } } internal static bool IsCallComplete( SteamAPICall_t call, out bool failed ) { failed = false; - return Internal.IsAPICallCompleted( call, ref failed ); + return Internal != null && Internal.IsAPICallCompleted( call, ref failed ); } /// /// Returns whether this steam client is a Steam China specific client, vs the global client /// - public static bool IsSteamChinaLauncher => Internal.IsSteamChinaLauncher(); + public static bool IsSteamChinaLauncher => Internal != null && Internal.IsSteamChinaLauncher(); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamVideo.cs b/Libraries/Facepunch.Steamworks/SteamVideo.cs index c6eaf438c..f3ec5fabe 100644 --- a/Libraries/Facepunch.Steamworks/SteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/SteamVideo.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamVideo : SteamClientClass { - internal static ISteamVideo Internal => Interface as ISteamVideo; + internal static ISteamVideo? Internal => Interface as ISteamVideo; internal override void InitializeInterface( bool server ) { @@ -26,8 +26,8 @@ namespace Steamworks Dispatch.Install( x => OnBroadcastStopped?.Invoke( x.Result ) ); } - public static event Action OnBroadcastStarted; - public static event Action OnBroadcastStopped; + public static event Action? OnBroadcastStarted; + public static event Action? OnBroadcastStopped; /// /// Return true if currently using Steam's live broadcasting @@ -37,7 +37,7 @@ namespace Steamworks get { int viewers = 0; - return Internal.IsBroadcasting( ref viewers ); + return Internal != null && Internal.IsBroadcasting( ref viewers ); } } @@ -50,7 +50,7 @@ namespace Steamworks { int viewers = 0; - if ( !Internal.IsBroadcasting( ref viewers ) ) + if ( Internal is null || !Internal.IsBroadcasting( ref viewers ) ) return 0; return viewers; diff --git a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs index 9f0feeaf2..eb731ad94 100644 --- a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs +++ b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs @@ -74,12 +74,10 @@ namespace Steamworks m_RulesFailedToRespond = onRulesFailedToRespond; m_RulesRefreshComplete = onRulesRefreshComplete; - m_VTable = new VTable() - { - m_VTRulesResponded = InternalOnRulesResponded, - m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, - m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete - }; + m_VTable = new VTable( + mVtRulesResponded: InternalOnRulesResponded, + mVtRulesFailedToRespond: InternalOnRulesFailedToRespond, + mVtRulesRefreshComplete: InternalOnRulesRefreshComplete); m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); Marshal.StructureToPtr(m_VTable, m_pVTable, false); @@ -153,13 +151,20 @@ namespace Steamworks private class VTable { [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesResponded m_VTRulesResponded; + public readonly InternalRulesResponded m_VTRulesResponded; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + public readonly InternalRulesFailedToRespond m_VTRulesFailedToRespond; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesRefreshComplete m_VTRulesRefreshComplete; + public readonly InternalRulesRefreshComplete m_VTRulesRefreshComplete; + + public VTable(InternalRulesResponded mVtRulesResponded, InternalRulesFailedToRespond mVtRulesFailedToRespond, InternalRulesRefreshComplete mVtRulesRefreshComplete) + { + m_VTRulesResponded = mVtRulesResponded; + m_VTRulesFailedToRespond = mVtRulesFailedToRespond; + m_VTRulesRefreshComplete = mVtRulesRefreshComplete; + } } public static explicit operator System.IntPtr(SteamMatchmakingRulesResponse that) diff --git a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs index 04ac43871..963596c7e 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs @@ -25,16 +25,16 @@ namespace Steamworks.Data get { var state = false; - SteamUserStats.Internal.GetAchievement( Value, ref state ); + SteamUserStats.Internal?.GetAchievement( Value, ref state ); return state; } } public string Identifier => Value; - public string Name => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "name" ); + public string? Name => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "name" ); - public string Description => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "desc" ); + public string? Description => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "desc" ); /// @@ -47,7 +47,7 @@ namespace Steamworks.Data var state = false; uint time = 0; - if ( !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) return null; return Epoch.ToDateTime( time ); @@ -60,6 +60,7 @@ namespace Steamworks.Data /// public Image? GetIcon() { + if (SteamUserStats.Internal is null) { return null; } return SteamUtils.GetImage( SteamUserStats.Internal.GetAchievementIcon( Value ) ); } @@ -69,6 +70,7 @@ namespace Steamworks.Data /// public async Task GetIconAsync( int timeout = 5000 ) { + if (SteamUserStats.Internal is null) { return null; } var i = SteamUserStats.Internal.GetAchievementIcon( Value ); if ( i != 0 ) return SteamUtils.GetImage( i ); @@ -115,7 +117,7 @@ namespace Steamworks.Data { float pct = 0; - if ( !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) return -1.0f; return pct / 100.0f; @@ -127,6 +129,8 @@ namespace Steamworks.Data /// public bool Trigger( bool apply = true ) { + if (SteamUserStats.Internal is null) { return false; } + var r = SteamUserStats.Internal.SetAchievement( Value ); if ( apply && r ) @@ -142,6 +146,7 @@ namespace Steamworks.Data /// public bool Clear() { + if (SteamUserStats.Internal is null) { return false; } return SteamUserStats.Internal.ClearAchievement( Value ); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Clan.cs b/Libraries/Facepunch.Steamworks/Structs/Clan.cs index 6bcdda826..e6cca31d5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Clan.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Clan.cs @@ -14,20 +14,20 @@ namespace Steamworks Id = id; } - public string Name => SteamFriends.Internal.GetClanName(Id); + public string? Name => SteamFriends.Internal?.GetClanName(Id); - public string Tag => SteamFriends.Internal.GetClanTag(Id); + public string? Tag => SteamFriends.Internal?.GetClanTag(Id); - public int ChatMemberCount => SteamFriends.Internal.GetClanChatMemberCount(Id); + public int ChatMemberCount => SteamFriends.Internal?.GetClanChatMemberCount(Id) ?? 0; - public Friend Owner => new Friend(SteamFriends.Internal.GetClanOwner(Id)); + public Friend Owner => new Friend(SteamFriends.Internal?.GetClanOwner(Id) ?? 0); - public bool Public => SteamFriends.Internal.IsClanPublic(Id); + public bool Public => SteamFriends.Internal != null && SteamFriends.Internal.IsClanPublic(Id); /// /// Is the clan an official game group? /// - public bool Official => SteamFriends.Internal.IsClanOfficialGameGroup(Id); + public bool Official => SteamFriends.Internal != null && SteamFriends.Internal.IsClanOfficialGameGroup(Id); /// /// Asynchronously fetches the officer list for a given clan @@ -35,14 +35,18 @@ namespace Steamworks /// Whether the request was successful or not public async Task RequestOfficerList() { + if (SteamFriends.Internal is null) { return false; } var req = await SteamFriends.Internal.RequestClanOfficerList(Id); return req.HasValue && req.Value.Success != 0x0; } public IEnumerable GetOfficers() { - for (int i = 0; i < SteamFriends.Internal.GetClanOfficerCount(Id); i++) + if (SteamFriends.Internal is null) { yield break; } + var officerCount = SteamFriends.Internal.GetClanOfficerCount(Id); + for (int i = 0; i < officerCount; i++) { + if (SteamFriends.Internal is null) { yield break; } yield return new Friend(SteamFriends.Internal.GetClanOfficerByIndex(Id, i)); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Controller.cs b/Libraries/Facepunch.Steamworks/Structs/Controller.cs index f694ecd2c..6fe355bd8 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Controller.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Controller.cs @@ -14,7 +14,7 @@ namespace Steamworks } public ulong Id => Handle.Value; - public InputType InputType => SteamInput.Internal.GetInputTypeForHandle( Handle ); + public InputType InputType => SteamInput.Internal?.GetInputTypeForHandle( Handle ) ?? InputType.Unknown; /// /// Reconfigure the controller to use the specified action set (ie 'Menu', 'Walk' or 'Drive') @@ -23,12 +23,12 @@ namespace Steamworks /// public string ActionSet { - set => SteamInput.Internal.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); + set => SteamInput.Internal?.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); } - public void DeactivateLayer( string layer ) => SteamInput.Internal.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ActivateLayer( string layer ) => SteamInput.Internal.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ClearLayers() => SteamInput.Internal.DeactivateAllActionSetLayers( Handle ); + public void DeactivateLayer( string layer ) => SteamInput.Internal?.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ActivateLayer( string layer ) => SteamInput.Internal?.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ClearLayers() => SteamInput.Internal?.DeactivateAllActionSetLayers( Handle ); /// @@ -36,7 +36,7 @@ namespace Steamworks /// public DigitalState GetDigitalState( string actionName ) { - return SteamInput.Internal.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ); + return SteamInput.Internal?.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ) ?? default; } /// @@ -44,7 +44,7 @@ namespace Steamworks /// public AnalogState GetAnalogState( string actionName ) { - return SteamInput.Internal.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ); + return SteamInput.Internal?.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ) ?? default; } diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 510227bbf..90a1df26f 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -73,16 +73,16 @@ namespace Steamworks - public Relationship Relationship => SteamFriends.Internal.GetFriendRelationship( Id ); - public FriendState State => SteamFriends.Internal.GetFriendPersonaState( Id ); - public string Name => SteamFriends.Internal.GetFriendPersonaName( Id ); + public Relationship Relationship => SteamFriends.Internal?.GetFriendRelationship( Id ) ?? Relationship.None; + public FriendState State => SteamFriends.Internal?.GetFriendPersonaState( Id ) ?? FriendState.Offline; + public string? Name => SteamFriends.Internal?.GetFriendPersonaName( Id ); public IEnumerable NameHistory { get { for( int i=0; i<32; i++ ) { - var n = SteamFriends.Internal.GetFriendPersonaNameHistory( Id, i ); + var n = SteamFriends.Internal?.GetFriendPersonaNameHistory( Id, i ); if ( string.IsNullOrEmpty( n ) ) break; @@ -91,7 +91,7 @@ namespace Steamworks } } - public int SteamLevel => SteamFriends.Internal.GetFriendSteamLevel( Id ); + public int SteamLevel => SteamFriends.Internal?.GetFriendSteamLevel( Id ) ?? 0; @@ -100,7 +100,7 @@ namespace Steamworks get { FriendGameInfo_t gameInfo = default; - if ( !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) + if ( SteamFriends.Internal is null || !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) return null; return FriendGameInfo.From( gameInfo ); @@ -109,7 +109,7 @@ namespace Steamworks public bool IsIn( SteamId group_or_room ) { - return SteamFriends.Internal.IsUserInSource( Id, group_or_room ); + return SteamFriends.Internal != null && SteamFriends.Internal.IsUserInSource( Id, group_or_room ); } public struct FriendGameInfo @@ -161,9 +161,9 @@ namespace Steamworks return await SteamFriends.GetLargeAvatarAsync( Id ); } - public string GetRichPresence( string key ) + public string? GetRichPresence( string key ) { - var val = SteamFriends.Internal.GetFriendRichPresence( Id, key ); + var val = SteamFriends.Internal?.GetFriendRichPresence( Id, key ); if ( string.IsNullOrEmpty( val ) ) return null; return val; } @@ -173,7 +173,7 @@ namespace Steamworks /// public bool InviteToGame( string Text ) { - return SteamFriends.Internal.InviteUserToGame( Id, Text ); + return SteamFriends.Internal != null && SteamFriends.Internal.InviteUserToGame( Id, Text ); } /// @@ -181,7 +181,7 @@ namespace Steamworks /// public bool SendMessage( string message ) { - return SteamFriends.Internal.ReplyToFriendMessage( Id, message ); + return SteamFriends.Internal != null && SteamFriends.Internal.ReplyToFriendMessage( Id, message ); } @@ -191,8 +191,9 @@ namespace Steamworks /// True if successful, False if failure public async Task RequestUserStatsAsync() { + if (SteamUserStats.Internal is null) { return false; } var result = await SteamUserStats.Internal.RequestUserStats( Id ); - return result.HasValue && result.Value.Result == Result.OK; + return result?.Result == Result.OK; } /// @@ -205,7 +206,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -221,7 +222,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -237,7 +238,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) return defult; return val; @@ -253,7 +254,7 @@ namespace Steamworks bool val = false; uint time = 0; - if ( !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) return DateTime.MinValue; return Epoch.ToDateTime( time ); diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs index 815094000..25afbd570 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs @@ -8,7 +8,7 @@ namespace Steamworks public class InventoryDef : IEquatable { internal InventoryDefId _id; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryDef( InventoryDefId defId ) { @@ -20,32 +20,32 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "name" ) /// - public string Name => GetProperty( "name" ); + public string? Name => GetProperty( "name" ); /// /// Shortcut to call GetProperty( "description" ) /// - public string Description => GetProperty( "description" ); + public string? Description => GetProperty( "description" ); /// /// Shortcut to call GetProperty( "icon_url" ) /// - public string IconUrl => GetProperty( "icon_url" ); + public string? IconUrl => GetProperty( "icon_url" ); /// /// Shortcut to call GetProperty( "icon_url_large" ) /// - public string IconUrlLarge => GetProperty( "icon_url_large" ); + public string? IconUrlLarge => GetProperty( "icon_url_large" ); /// /// Shortcut to call GetProperty( "price_category" ) /// - public string PriceCategory => GetProperty( "price_category" ); + public string? PriceCategory => GetProperty( "price_category" ); /// /// Shortcut to call GetProperty( "type" ) /// - public string Type => GetProperty( "type" ); + public string? Type => GetProperty( "type" ); /// /// Returns true if this is an item that generates an item, rather @@ -56,12 +56,12 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "exchange" ) /// - public string ExchangeSchema => GetProperty( "exchange" ); + public string? ExchangeSchema => GetProperty( "exchange" ); /// /// Get a list of exchanges that are available to make this item /// - public InventoryRecipe[] GetRecipes() + public InventoryRecipe[]? GetRecipes() { if ( string.IsNullOrEmpty( ExchangeSchema ) ) return null; @@ -93,19 +93,19 @@ namespace Steamworks /// /// Get a specific property by name /// - public string GetProperty( string name ) + public string? GetProperty( string? name ) { - if ( _properties!= null && _properties.TryGetValue( name, out string val ) ) + if ( _properties != null && name != null && _properties.TryGetValue( name, out string val ) ) return val; uint _ = (uint)Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) return null; - + if (name == null) //return keys string return vl; - + if ( _properties == null ) _properties = new Dictionary(); @@ -119,9 +119,9 @@ namespace Steamworks /// public bool GetBoolProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); - if ( val.Length == 0 ) return false; + if ( string.IsNullOrEmpty(val) ) return false; if ( val[0] == '0' || val[0] == 'F' || val[0] == 'f' ) return false; return true; @@ -130,9 +130,9 @@ namespace Steamworks /// /// Read a raw property from the definition schema /// - public T GetProperty( string name ) + public T? GetProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); if ( string.IsNullOrEmpty( val ) ) return default; @@ -150,16 +150,16 @@ namespace Steamworks /// /// Gets a list of all properties on this item /// - public IEnumerable> Properties + public IEnumerable> Properties { get { - var list = GetProperty( null ); + var list = GetProperty( null ) ?? ""; var keys = list.Split( ',' ); foreach ( var key in keys ) { - yield return new KeyValuePair( key, GetProperty( key ) ); + yield return new KeyValuePair( key, GetProperty( key ) ); } } } @@ -174,7 +174,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int) curprice; @@ -194,7 +194,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int)baseprice; @@ -203,12 +203,12 @@ namespace Steamworks public string LocalBasePriceFormatted => Utility.FormatPrice( SteamInventory.Currency, LocalPrice / 100.0 ); - InventoryRecipe[] _recContaining; + InventoryRecipe[]? _recContaining; /// /// Return a list of recepies that contain this item /// - public InventoryRecipe[] GetRecipesContainingThis() + public InventoryRecipe[]? GetRecipesContainingThis() { if ( _recContaining != null ) return _recContaining; @@ -221,17 +221,17 @@ namespace Steamworks return _recContaining; } - public static bool operator ==( InventoryDef a, InventoryDef b ) + public static bool operator ==( InventoryDef? a, InventoryDef? b ) { if ( Object.ReferenceEquals( a, null ) ) return Object.ReferenceEquals( b, null ); return a.Equals( b ); } - public static bool operator !=( InventoryDef a, InventoryDef b ) => !(a == b); + public static bool operator !=( InventoryDef? a, InventoryDef? b ) => !(a == b); public override bool Equals( object p ) => this.Equals( (InventoryDef)p ); public override int GetHashCode() => Id.GetHashCode(); - public bool Equals( InventoryDef p ) + public bool Equals( InventoryDef? p ) { if ( p == null ) return false; return p.Id == Id; diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs index 886ed9be9..aeee9a5e3 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs @@ -11,7 +11,7 @@ namespace Steamworks internal InventoryDefId _def; internal SteamItemFlags _flags; internal ushort _quantity; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryItemId Id => _id; @@ -19,13 +19,13 @@ namespace Steamworks public int Quantity => _quantity; - public InventoryDef Def => SteamInventory.FindDefinition( DefId ); + public InventoryDef? Def => SteamInventory.FindDefinition( DefId ); /// /// Only available if the result set was created with the getproperties /// - public Dictionary Properties => _properties; + public Dictionary? Properties => _properties; /// /// This item is account-locked and cannot be traded or given away. @@ -54,7 +54,7 @@ namespace Steamworks public async Task ConsumeAsync( int amount = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -66,7 +66,7 @@ namespace Steamworks public async Task SplitStackAsync( int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -78,7 +78,7 @@ namespace Steamworks public async Task AddAsync( InventoryItem add, int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -98,11 +98,11 @@ namespace Steamworks return i; } - internal static Dictionary GetProperties( SteamInventoryResult_t result, int index ) + internal static Dictionary? GetProperties( SteamInventoryResult_t result, int index ) { var strlen = (uint) Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) return null; var props = new Dictionary(); @@ -151,7 +151,7 @@ namespace Steamworks /// Tries to get the origin property. Need properties for this to work. /// Will return a string like "market" /// - public string Origin + public string? Origin { get { diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs index 1305eb8d0..21440664d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs @@ -22,7 +22,7 @@ namespace Steamworks /// If we don't know about this item definition this might be null. /// In which case, DefinitionId should still hold the correct id. /// - public InventoryDef Definition; + public InventoryDef? Definition; /// /// The amount of this item needed. Generally this will be 1. diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs index 5518ba4ed..8c58353b1 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs @@ -23,7 +23,7 @@ namespace Steamworks { uint cnt = 0; - if ( !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) return 0; return (int) cnt; @@ -36,17 +36,17 @@ namespace Steamworks /// public bool BelongsTo( SteamId steamId ) { - return SteamInventory.Internal.CheckResultSteamID( _id, steamId ); + return SteamInventory.Internal != null && SteamInventory.Internal.CheckResultSteamID( _id, steamId ); } - public InventoryItem[] GetItems( bool includeProperties = false ) + public InventoryItem[]? GetItems( bool includeProperties = false ) { uint cnt = (uint) ItemCount; if ( cnt <= 0 ) return null; var pOutItemsArray = new SteamItemDetails_t[cnt]; - if ( !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) return null; var items = new InventoryItem[cnt]; @@ -69,7 +69,7 @@ namespace Steamworks { if ( _id.Value == -1 ) return; - SteamInventory.Internal.DestroyResult( _id ); + SteamInventory.Internal?.DestroyResult( _id ); } internal static async Task GetAsync( SteamInventoryResult_t sresult ) @@ -77,7 +77,7 @@ namespace Steamworks var _result = Result.Pending; while ( _result == Result.Pending ) { - _result = SteamInventory.Internal.GetResultStatus( sresult ); + _result = SteamInventory.Internal?.GetResultStatus( sresult ) ?? Result.Fail; await Task.Delay( 10 ); } @@ -97,11 +97,11 @@ namespace Steamworks /// Results have a built-in timestamp which will be considered "expired" after an hour has elapsed.See DeserializeResult /// for expiration handling. /// - public unsafe byte[] Serialize() + public unsafe byte[]? Serialize() { uint size = 0; - if ( !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) return null; var data = new byte[size]; diff --git a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs index ed7deb8c6..38c0dc1e0 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs @@ -14,10 +14,10 @@ namespace Steamworks.Data /// /// the name of a leaderboard /// - public string Name => SteamUserStats.Internal.GetLeaderboardName( Id ); - public LeaderboardSort Sort => SteamUserStats.Internal.GetLeaderboardSortMethod( Id ); - public LeaderboardDisplay Display => SteamUserStats.Internal.GetLeaderboardDisplayType( Id ); - public int EntryCount => SteamUserStats.Internal.GetLeaderboardEntryCount(Id); + public string? Name => SteamUserStats.Internal?.GetLeaderboardName( Id ); + public LeaderboardSort Sort => SteamUserStats.Internal?.GetLeaderboardSortMethod( Id ) ?? default; + public LeaderboardDisplay Display => SteamUserStats.Internal?.GetLeaderboardDisplayType( Id ) ?? default; + public int EntryCount => SteamUserStats.Internal?.GetLeaderboardEntryCount(Id) ?? 0; static int[] detailsBuffer = new int[64]; static int[] noDetails = Array.Empty(); @@ -25,8 +25,9 @@ namespace Steamworks.Data /// /// Submit your score and replace your old score even if it was better /// - public async Task ReplaceScore( int score, int[] details = null ) + public async Task ReplaceScore( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.ForceUpdate, score, details, details.Length ); @@ -38,8 +39,9 @@ namespace Steamworks.Data /// /// Submit your new score, but won't replace your high score if it's lower /// - public async Task SubmitScoreAsync( int score, int[] details = null ) + public async Task SubmitScoreAsync( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.KeepBest, score, details, details.Length ); @@ -53,6 +55,7 @@ namespace Steamworks.Data /// public async Task AttachUgc( Ugc file ) { + if (SteamUserStats.Internal is null) { return Result.Fail; } var r = await SteamUserStats.Internal.AttachLeaderboardUGC( Id, file.Handle ); if ( !r.HasValue ) return Result.Fail; @@ -62,8 +65,9 @@ namespace Steamworks.Data /// /// Fetches leaderboard entries for an arbitrary set of users on a specified leaderboard. /// - public async Task GetScoresForUsersAsync( SteamId[] users ) + public async Task GetScoresForUsersAsync( SteamId[]? users ) { + if (SteamUserStats.Internal is null) { return null; } if ( users == null || users.Length == 0 ) return null; @@ -77,8 +81,9 @@ namespace Steamworks.Data /// /// Used to query for a sequential range of leaderboard entries by leaderboard Sort. /// - public async Task GetScoresAsync( int count, int offset = 1 ) + public async Task GetScoresAsync( int count, int offset = 1 ) { + if (SteamUserStats.Internal is null) { return null; } if ( offset <= 0 ) throw new System.ArgumentException( "Should be 1+", nameof( offset ) ); var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Global, offset, offset + count - 1 ); @@ -94,8 +99,9 @@ namespace Steamworks.Data /// For example, if the user is #1 on the leaderboard and start is set to -2, end is set to 2, Steam will return the first /// 5 entries in the leaderboard. If The current user has no entry, this will return null. /// - public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) + public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.GlobalAroundUser, start, end ); if ( !r.HasValue ) return null; @@ -106,8 +112,9 @@ namespace Steamworks.Data /// /// Used to retrieve all leaderboard entries for friends of the current user /// - public async Task GetScoresFromFriendsAsync() + public async Task GetScoresFromFriendsAsync() { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Friends, 0, 0 ); if ( !r.HasValue ) return null; @@ -116,8 +123,9 @@ namespace Steamworks.Data } #region util - internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) + internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) { + if (SteamUserStats.Internal is null) { return null; } if ( r.CEntryCount <= 0 ) return null; @@ -142,6 +150,8 @@ namespace Steamworks.Data bool gotAll = false; while ( !gotAll ) { + if (SteamFriends.Internal is null) { return; } + gotAll = true; foreach ( var entry in entries ) diff --git a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs index 82eb26fb7..6758da17d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs @@ -7,7 +7,7 @@ namespace Steamworks.Data public Friend User; public int GlobalRank; public int Score; - public int[] Details; + public int[]? Details; // UGCHandle_t m_hUGC internal static LeaderboardEntry From( LeaderboardEntry_t e, int[] detailsBuffer ) diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 2b0fdb57a..397e82350 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -23,6 +23,8 @@ namespace Steamworks.Data /// public async Task Join() { + if (SteamMatchmaking.Internal is null) { return RoomEnter.Error; } + var result = await SteamMatchmaking.Internal.JoinLobby( Id ); if ( !result.HasValue ) return RoomEnter.Error; @@ -35,7 +37,7 @@ namespace Steamworks.Data /// public void Leave() { - SteamMatchmaking.Internal.LeaveLobby( Id ); + SteamMatchmaking.Internal?.LeaveLobby( Id ); } /// @@ -45,13 +47,13 @@ namespace Steamworks.Data /// public bool InviteFriend( SteamId steamid ) { - return SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); } /// /// returns the number of users in the specified lobby /// - public int MemberCount => SteamMatchmaking.Internal.GetNumLobbyMembers( Id ); + public int MemberCount => SteamMatchmaking.Internal?.GetNumLobbyMembers( Id ) ?? 0; /// /// Returns current members. Need to be in the lobby to see the users. @@ -62,6 +64,7 @@ namespace Steamworks.Data { for( int i = 0; i < MemberCount; i++ ) { + if (SteamMatchmaking.Internal is null) { break; } yield return new Friend( SteamMatchmaking.Internal.GetLobbyMemberByIndex( Id, i ) ); } } @@ -71,9 +74,9 @@ namespace Steamworks.Data /// /// Get data associated with this lobby /// - public string GetData( string key ) + public string? GetData( string key ) { - return SteamMatchmaking.Internal.GetLobbyData( Id, key ); + return SteamMatchmaking.Internal?.GetLobbyData( Id, key ); } /// @@ -84,7 +87,7 @@ namespace Steamworks.Data if ( key.Length > 255 ) throw new System.ArgumentException( "Key should be < 255 chars", nameof( key ) ); if ( value.Length > 8192 ) throw new System.ArgumentException( "Value should be < 8192 chars", nameof( key ) ); - return SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); } /// @@ -92,7 +95,7 @@ namespace Steamworks.Data /// public bool DeleteData( string key ) { - return SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); } /// @@ -102,10 +105,11 @@ namespace Steamworks.Data { get { - var cnt = SteamMatchmaking.Internal.GetLobbyDataCount( Id ); + var cnt = SteamMatchmaking.Internal?.GetLobbyDataCount( Id ) ?? 0; for ( int i =0; i( a, b ); @@ -117,9 +121,9 @@ namespace Steamworks.Data /// /// Gets per-user metadata for someone in this lobby /// - public string GetMemberData( Friend member, string key ) + public string? GetMemberData( Friend member, string key ) { - return SteamMatchmaking.Internal.GetLobbyMemberData( Id, member.Id, key ); + return SteamMatchmaking.Internal?.GetLobbyMemberData( Id, member.Id, key ); } /// @@ -127,7 +131,7 @@ namespace Steamworks.Data /// public void SetMemberData( string key, string value ) { - SteamMatchmaking.Internal.SetLobbyMemberData( Id, key, value ); + SteamMatchmaking.Internal?.SetLobbyMemberData( Id, key, value ); } /// @@ -148,7 +152,7 @@ namespace Steamworks.Data { fixed ( byte* ptr = data ) { - return SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); } } @@ -163,7 +167,7 @@ namespace Steamworks.Data /// public bool Refresh() { - return SteamMatchmaking.Internal.RequestLobbyData( Id ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.RequestLobbyData( Id ); } /// @@ -172,33 +176,33 @@ namespace Steamworks.Data /// public int MaxMembers { - get => SteamMatchmaking.Internal.GetLobbyMemberLimit( Id ); - set => SteamMatchmaking.Internal.SetLobbyMemberLimit( Id, value ); + get => SteamMatchmaking.Internal?.GetLobbyMemberLimit( Id ) ?? 0; + set => SteamMatchmaking.Internal?.SetLobbyMemberLimit( Id, value ); } public bool SetPublic() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); } public bool SetPrivate() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); } public bool SetInvisible() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); } public bool SetFriendsOnly() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); } public bool SetJoinable( bool b ) { - return SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); } /// @@ -211,7 +215,7 @@ namespace Steamworks.Data if ( !steamServer.IsValid ) throw new ArgumentException( $"SteamId for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, 0, 0, steamServer ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, 0, 0, steamServer ); } /// @@ -224,7 +228,7 @@ namespace Steamworks.Data if ( !IPAddress.TryParse( ip, out IPAddress add ) ) throw new ArgumentException( $"IP address for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); } /// @@ -233,7 +237,7 @@ namespace Steamworks.Data /// public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId ) { - return SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); } /// @@ -241,8 +245,8 @@ namespace Steamworks.Data /// public Friend Owner { - get => new Friend( SteamMatchmaking.Internal.GetLobbyOwner( Id ) ); - set => SteamMatchmaking.Internal.SetLobbyOwner( Id, value.Id ); + get => new Friend( SteamMatchmaking.Internal?.GetLobbyOwner( Id ) ?? 0 ); + set => SteamMatchmaking.Internal?.SetLobbyOwner( Id, value.Id ); } /// diff --git a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs index 1401e5820..4a55ef640 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs @@ -197,6 +197,8 @@ namespace Steamworks.Data void ApplyFilters() { + if (SteamMatchmaking.Internal is null) { return; } + if ( distance.HasValue ) { SteamMatchmaking.Internal.AddRequestLobbyListDistanceFilter( distance.Value ); @@ -251,8 +253,10 @@ namespace Steamworks.Data /// /// Run the query, get the matching lobbies /// - public async Task RequestAsync() + public async Task RequestAsync() { + if (SteamMatchmaking.Internal is null) { return null; } + await Task.Yield(); ApplyFilters(); diff --git a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs index b88904221..5254c9f50 100644 --- a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs +++ b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs @@ -5,7 +5,7 @@ namespace Steamworks { public struct PartyBeacon { - static ISteamParties Internal => SteamParties.Internal; + static ISteamParties? Internal => SteamParties.Internal; internal PartyBeaconID_t Id; @@ -18,7 +18,7 @@ namespace Steamworks { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - Internal.GetBeaconDetails( Id, ref owner, ref location, out _ ); + Internal?.GetBeaconDetails( Id, ref owner, ref location, out _ ); return owner; } } @@ -26,13 +26,14 @@ namespace Steamworks /// /// Creator of the beacon /// - public string MetaData + public string? MetaData { get { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - _ = Internal.GetBeaconDetails( Id, ref owner, ref location, out var strVal ); + string? strVal = null; + _ = Internal?.GetBeaconDetails( Id, ref owner, ref location, out strVal ); return strVal; } } @@ -41,8 +42,10 @@ namespace Steamworks /// Will attempt to join the party. If successful will return a connection string. /// If failed, will return null /// - public async Task JoinAsync() + public async Task JoinAsync() { + if (Internal is null) { return null; } + var result = await Internal.JoinParty( Id ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; @@ -56,7 +59,7 @@ namespace Steamworks /// public void OnReservationCompleted( SteamId steamid ) { - Internal.OnReservationCompleted( Id, steamid ); + Internal?.OnReservationCompleted( Id, steamid ); } /// @@ -66,7 +69,7 @@ namespace Steamworks /// public void CancelReservation( SteamId steamid ) { - Internal.CancelReservation( Id, steamid ); + Internal?.CancelReservation( Id, steamid ); } /// @@ -74,7 +77,7 @@ namespace Steamworks /// public bool Destroy() { - return Internal.DestroyBeacon( Id ); + return Internal != null && Internal.DestroyBeacon( Id ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs index f39693fce..655ab288d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs +++ b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs @@ -23,16 +23,16 @@ namespace Steamworks.Data /// /// Get the SteamID of the connected user /// - public SteamId SteamId => SteamRemotePlay.Internal.GetSessionSteamID( Id ); + public SteamId SteamId => SteamRemotePlay.Internal?.GetSessionSteamID( Id ) ?? default; /// /// Get the name of the session client device /// - public string ClientName => SteamRemotePlay.Internal.GetSessionClientName( Id ); + public string? ClientName => SteamRemotePlay.Internal?.GetSessionClientName( Id ); /// /// Get the name of the session client device /// - public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal.GetSessionClientFormFactor( Id ); + public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal?.GetSessionClientFormFactor( Id ) ?? SteamDeviceFormFactor.Unknown; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs index 2d6e92010..a30b506e4 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs @@ -15,7 +15,7 @@ namespace Steamworks.Data /// public bool TagUser( SteamId user ) { - return SteamScreenshots.Internal.TagUser( Value, user ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagUser( Value, user ); } /// @@ -23,7 +23,7 @@ namespace Steamworks.Data /// public bool SetLocation( string location ) { - return SteamScreenshots.Internal.SetLocation( Value, location ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.SetLocation( Value, location ); } /// @@ -31,7 +31,7 @@ namespace Steamworks.Data /// public bool TagPublishedFile( PublishedFileId file ) { - return SteamScreenshots.Internal.TagPublishedFile( Value, file ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagPublishedFile( Value, file ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/Server.cs b/Libraries/Facepunch.Steamworks/Structs/Server.cs index 89ea2243a..5e74b0a35 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Server.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Server.cs @@ -9,11 +9,11 @@ namespace Steamworks.Data { public struct ServerInfo : IEquatable { - public string Name { get; set; } + public string? Name { get; set; } public int Ping { get; set; } - public string GameDir { get; set; } - public string Map { get; set; } - public string Description { get; set; } + public string? GameDir { get; set; } + public string? Map { get; set; } + public string? Description { get; set; } public uint AppId { get; set; } public int Players { get; set; } public int MaxPlayers { get; set; } @@ -22,19 +22,19 @@ namespace Steamworks.Data public bool Secure { get; set; } public uint LastTimePlayed { get; set; } public int Version { get; set; } - public string TagString { get; set; } + public string? TagString { get; set; } public ulong SteamId { get; set; } public uint AddressRaw { get; set; } - public IPAddress Address { get; set; } + public IPAddress? Address { get; set; } public int ConnectionPort { get; set; } public int QueryPort { get; set; } - string[] _tags; + string[]? _tags; /// /// Gets the individual tags for this server /// - public string[] Tags + public string[]? Tags { get { @@ -97,13 +97,13 @@ namespace Steamworks.Data /// public void AddToHistory() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); } /// /// If this server responds to source engine style queries, we'll be able to get a list of rules here /// - public async Task> QueryRulesAsync() + public async Task?> QueryRulesAsync() { return await SourceServerQuery.GetRules( this ); } @@ -113,7 +113,7 @@ namespace Steamworks.Data /// public void RemoveFromHistory() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); } /// @@ -121,7 +121,7 @@ namespace Steamworks.Data /// public void AddToFavourites() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); } /// @@ -129,7 +129,7 @@ namespace Steamworks.Data /// public void RemoveFromFavourites() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); } public bool Equals( ServerInfo other ) @@ -139,7 +139,7 @@ namespace Steamworks.Data public override int GetHashCode() { - return Address.GetHashCode() + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); + return (Address?.GetHashCode() ?? 0) + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 7664a9bb1..637c85c06 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -21,7 +21,7 @@ namespace Steamworks /// public struct SteamServerInit { - public IPAddress IpAddress; + public IPAddress? IpAddress; public ushort SteamPort; public ushort GamePort; public ushort QueryPort; diff --git a/Libraries/Facepunch.Steamworks/Structs/Stat.cs b/Libraries/Facepunch.Steamworks/Structs/Stat.cs index 559fb6954..505ffeb4b 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Stat.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Stat.cs @@ -26,7 +26,7 @@ namespace Steamworks.Data UserId = user; } - internal void LocalUserOnly( [CallerMemberName] string caller = null ) + internal void LocalUserOnly( [CallerMemberName] string? caller = null ) { if ( UserId == 0 ) return; throw new System.Exception( $"Stat.{caller} can only be called for the local user" ); @@ -36,7 +36,7 @@ namespace Steamworks.Data { double val = 0.0; - if ( SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) + if ( SteamUserStats.Internal != null && SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) return val; return 0; @@ -45,12 +45,14 @@ namespace Steamworks.Data public long GetGlobalInt() { long val = 0; - SteamUserStats.Internal.GetGlobalStat( Name, ref val ); + SteamUserStats.Internal?.GetGlobalStat( Name, ref val ); return val; } - public async Task GetGlobalIntDaysAsync( int days ) + public async Task GetGlobalIntDaysAsync( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -64,8 +66,10 @@ namespace Steamworks.Data return r; } - public async Task GetGlobalFloatDays( int days ) + public async Task GetGlobalFloatDays( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -85,14 +89,14 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } - return 0; + return val; } public int GetInt() @@ -101,11 +105,11 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } return val; @@ -114,13 +118,13 @@ namespace Steamworks.Data public bool Set( int val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Set( float val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Add( int val ) @@ -138,13 +142,13 @@ namespace Steamworks.Data public bool UpdateAverageRate( float count, float sessionlength ) { LocalUserOnly(); - return SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); } public bool Store() { LocalUserOnly(); - return SteamUserStats.Internal.StoreStats(); + return SteamUserStats.Internal != null && SteamUserStats.Internal.StoreStats(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 9acaf229c..1067141b5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -47,25 +47,25 @@ namespace Steamworks.Ugc public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } - public string Title { get; private set; } + public string? Title { get; private set; } public Editor WithTitle( string t ) { this.Title = t; return this; } - public string Description { get; private set; } + public string? Description { get; private set; } public Editor WithDescription( string t ) { this.Description = t; return this; } - string MetaData; + string? MetaData; public Editor WithMetaData( string t ) { this.MetaData = t; return this; } - string ChangeLog; + string? ChangeLog; public Editor WithChangeLog( string t ) { this.ChangeLog = t; return this; } - string Language; + string? Language; public Editor InLanguage( string t ) { this.Language = t; return this; } - public string PreviewFile { get; private set; } - public Editor WithPreviewFile( string t ) { this.PreviewFile = t; return this; } + public string? PreviewFile { get; private set; } + public Editor WithPreviewFile( string? t ) { this.PreviewFile = t; return this; } - public System.IO.DirectoryInfo ContentFolder { get; private set; } + public System.IO.DirectoryInfo? ContentFolder { get; private set; } public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; } public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); } @@ -73,9 +73,9 @@ namespace Steamworks.Ugc public Editor WithVisibility(Visibility visibility) { Visibility = visibility; return this; } - public List Tags { get; private set; } - Dictionary> keyValueTags; - HashSet keyValueTagsToRemove; + public List? Tags { get; private set; } + Dictionary>? keyValueTags; + HashSet? keyValueTagsToRemove; public Editor WithTag( string tag ) { @@ -143,9 +143,10 @@ namespace Steamworks.Ugc return false; } - public async Task SubmitAsync( IProgress progress = null ) + public async Task SubmitAsync( IProgress? progress = null ) { var result = default( PublishResult ); + if (SteamUGC.Internal is null) { return result; } progress?.Report( 0 ); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 7a69e3819..49ff42292 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -35,22 +35,22 @@ namespace Steamworks.Ugc /// /// The given title of this item /// - public string Title { get; internal set; } + public string? Title { get; internal set; } /// /// The description of this item, in your local language if available /// - public string Description { get; internal set; } + public string? Description { get; internal set; } /// /// A list of tags for this item, all lowercase /// - public string[] Tags { get; internal set; } + public string[]? Tags { get; internal set; } /// /// A dictionary of key value tags for this item, only available from queries WithKeyValueTags(true) /// - public Dictionary KeyValueTags { get; internal set; } + public Dictionary? KeyValueTags { get; internal set; } /// /// App Id of the app that created this item @@ -123,14 +123,14 @@ namespace Steamworks.Ugc public bool IsSubscribed => (State & ItemState.Subscribed) == ItemState.Subscribed; public bool NeedsUpdate => (State & ItemState.NeedsUpdate) == ItemState.NeedsUpdate; - public string Directory + public string? Directory { get { ulong size = 0; uint ts = 0; - if (SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } return null; } } @@ -147,7 +147,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long) total; return -1; @@ -166,7 +166,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long)downloaded; return -1; @@ -185,7 +185,7 @@ namespace Steamworks.Ugc ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return 0; return (long) size; @@ -201,7 +201,7 @@ namespace Steamworks.Ugc { ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return null; return Epoch.ToDateTime(ts); @@ -226,7 +226,7 @@ namespace Steamworks.Ugc //possibly similar properties should also be changed ulong downloaded = 0; ulong total = 0; - if (SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) { return (float)((double)downloaded / (double)total); } @@ -277,7 +277,7 @@ namespace Steamworks.Ugc /// public bool HasTag( string find ) { - if ( Tags.Length == 0 ) return false; + if ( Tags is null || Tags.Length == 0 ) return false; return Tags.Contains( find, StringComparer.OrdinalIgnoreCase ); } @@ -287,6 +287,7 @@ namespace Steamworks.Ugc /// public async Task Subscribe () { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.SubscribeItem( _id ); return result?.Result == Result.OK; } @@ -296,7 +297,7 @@ namespace Steamworks.Ugc /// If CancellationToken is default then there is 60 seconds timeout /// Progress will be set to 0-1 /// - public async Task DownloadAsync( Action progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) + public async Task DownloadAsync( Action? progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) { return await SteamUGC.DownloadAsync( Id, progress, milisecondsUpdateDelay, ct ); } @@ -305,7 +306,8 @@ namespace Steamworks.Ugc /// Allows the user to unsubscribe from this item /// public async Task Unsubscribe () - { + { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.UnsubscribeItem( _id ); return result?.Result == Result.OK; } @@ -315,6 +317,7 @@ namespace Steamworks.Ugc /// public async Task AddFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.AddItemToFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -324,6 +327,7 @@ namespace Steamworks.Ugc /// public async Task RemoveFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.RemoveItemFromFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -333,6 +337,7 @@ namespace Steamworks.Ugc /// public async Task Vote( bool up ) { + if (SteamUGC.Internal is null) { return null; } var r = await SteamUGC.Internal.SetUserItemVote( Id, up ); return r?.Result; } @@ -342,6 +347,7 @@ namespace Steamworks.Ugc /// public async Task GetUserVote() { + if (SteamUGC.Internal is null) { return null; } var result = await SteamUGC.Internal.GetUserItemVote(_id); if (!result.HasValue) return null; @@ -351,27 +357,27 @@ namespace Steamworks.Ugc /// /// Return a URL to view this item online /// - public string Url => $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; + public string Url => $"https://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; /// /// The URl to view this item's changelog /// - public string ChangelogUrl => $"http://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; + public string ChangelogUrl => $"https://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; /// /// The URL to view the comments on this item /// - public string CommentsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; + public string CommentsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; /// /// The URL to discuss this item /// - public string DiscussUrl => $"http://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; + public string DiscussUrl => $"https://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; /// /// The URL to view this items stats online /// - public string StatsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; + public string StatsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; public ulong NumSubscriptions { get; internal set; } public ulong NumFavorites { get; internal set; } @@ -390,12 +396,12 @@ namespace Steamworks.Ugc /// /// The URL to the preview image for this item /// - public string PreviewImageUrl { get; internal set; } + public string? PreviewImageUrl { get; internal set; } /// /// The metadata string for this item, only available from queries WithMetadata(true) /// - public string Metadata { get; internal set; } + public string? Metadata { get; internal set; } /// /// Edit this item diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index b8bc42740..cfb65ca75 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -14,7 +14,7 @@ namespace Steamworks.Ugc UGCQuery queryType; AppId consumerApp; AppId creatorApp; - string searchText; + string? searchText; public Query( UgcType type ) : this() { @@ -96,7 +96,7 @@ namespace Steamworks.Ugc #endregion #region Files - PublishedFileId[] Files; + PublishedFileId[]? Files; public Query WithFileId( params PublishedFileId[] files ) { @@ -109,6 +109,8 @@ namespace Steamworks.Ugc { if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); + if (SteamUGC.Internal is null) { return null; } + if ( consumerApp == 0 ) consumerApp = SteamClient.AppId; if ( creatorApp == 0 ) creatorApp = consumerApp; @@ -159,16 +161,16 @@ namespace Steamworks.Ugc public QueryType WithType( UgcType type ) { matchingType = type; return this; } int? maxCacheAge; public QueryType AllowCachedResponse( int maxSecondsAge ) { maxCacheAge = maxSecondsAge; return this; } - string language; + string? language; public QueryType InLanguage( string lang ) { language = lang; return this; } int? trendDays; public QueryType WithTrendDays( int days ) { trendDays = days; return this; } - List requiredTags; + List? requiredTags; bool? matchAnyTag; - List excludedTags; - Dictionary requiredKv; + List? excludedTags; + Dictionary? requiredKv; /// /// Found items must have at least one of the defined tags @@ -213,34 +215,34 @@ namespace Steamworks.Ugc if ( requiredTags != null ) { foreach ( var tag in requiredTags ) - SteamUGC.Internal.AddRequiredTag( handle, tag ); + SteamUGC.Internal?.AddRequiredTag( handle, tag ); } if ( excludedTags != null ) { foreach ( var tag in excludedTags ) - SteamUGC.Internal.AddExcludedTag( handle, tag ); + SteamUGC.Internal?.AddExcludedTag( handle, tag ); } if ( requiredKv != null ) { foreach ( var tag in requiredKv ) - SteamUGC.Internal.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); + SteamUGC.Internal?.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); } if ( matchAnyTag.HasValue ) { - SteamUGC.Internal.SetMatchAnyTag( handle, matchAnyTag.Value ); + SteamUGC.Internal?.SetMatchAnyTag( handle, matchAnyTag.Value ); } if ( trendDays.HasValue ) { - SteamUGC.Internal.SetRankedByTrendDays( handle, (uint)trendDays.Value ); + SteamUGC.Internal?.SetRankedByTrendDays( handle, (uint)trendDays.Value ); } if ( !string.IsNullOrEmpty( searchText ) ) { - SteamUGC.Internal.SetSearchText( handle, searchText ); + SteamUGC.Internal?.SetSearchText( handle, searchText ); } } @@ -271,42 +273,42 @@ namespace Steamworks.Ugc { if (WantsReturnOnlyIDs.HasValue) { - SteamUGC.Internal.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); + SteamUGC.Internal?.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); } if (WantsReturnKeyValueTags.HasValue) { - SteamUGC.Internal.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); + SteamUGC.Internal?.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); } if (WantsReturnLongDescription.HasValue) { - SteamUGC.Internal.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); + SteamUGC.Internal?.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); } if (WantsReturnMetadata.HasValue) { - SteamUGC.Internal.SetReturnMetadata(handle, WantsReturnMetadata.Value); + SteamUGC.Internal?.SetReturnMetadata(handle, WantsReturnMetadata.Value); } if (WantsReturnChildren.HasValue) { - SteamUGC.Internal.SetReturnChildren(handle, WantsReturnChildren.Value); + SteamUGC.Internal?.SetReturnChildren(handle, WantsReturnChildren.Value); } if (WantsReturnAdditionalPreviews.HasValue) { - SteamUGC.Internal.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); + SteamUGC.Internal?.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); } if (WantsReturnTotalOnly.HasValue) { - SteamUGC.Internal.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); + SteamUGC.Internal?.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); } if (WantsReturnPlaytimeStats.HasValue) { - SteamUGC.Internal.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); + SteamUGC.Internal?.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs index 86a6e2b51..54ffe6973 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs @@ -24,6 +24,7 @@ namespace Steamworks.Ugc var details = default( SteamUGCDetails_t ); for ( uint i=0; i< ResultCount; i++ ) { + if (SteamUGC.Internal is null) { yield break; } if ( SteamUGC.Internal.GetQueryUGCResult( Handle, i, ref details ) ) { var item = Item.From( details ); @@ -86,7 +87,7 @@ namespace Steamworks.Ugc { ulong val = 0; - if ( !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) return 0; return val; @@ -96,7 +97,7 @@ namespace Steamworks.Ugc { if ( Handle > 0 ) { - SteamUGC.Internal.ReleaseQueryUGCRequest( Handle ); + SteamUGC.Internal?.ReleaseQueryUGCRequest( Handle ); Handle = 0; } } diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index bc1c4d848..0f35438bd 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -16,13 +16,13 @@ namespace Steamworks private static readonly HashSet ruleResponseHandlers = new HashSet(); - internal static async Task> GetRules(Steamworks.Data.ServerInfo server) + internal static async Task?> GetRules(Steamworks.Data.ServerInfo server) { Status status = Status.Pending; var rules = new Dictionary(); - SteamMatchmakingRulesResponse responseHandler = null; + SteamMatchmakingRulesResponse? responseHandler = null; void onRulesResponded(string key, string value) => rules.Add(key, value); @@ -51,6 +51,8 @@ namespace Steamworks responseHandler = null; } + if (SteamMatchmakingServers.Internal is null) { return null; } + responseHandler = new SteamMatchmakingRulesResponse( onRulesResponded, onRulesFailToRespond, diff --git a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs index a8b13dab4..ca0b5a20c 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs @@ -61,9 +61,9 @@ namespace Steamworks public class SteamSharedClass : SteamClass { - internal static SteamInterface Interface => InterfaceClient ?? InterfaceServer; - internal static SteamInterface InterfaceClient; - internal static SteamInterface InterfaceServer; + internal static SteamInterface? Interface => InterfaceClient ?? InterfaceServer; + internal static SteamInterface? InterfaceClient; + internal static SteamInterface? InterfaceServer; internal override void InitializeInterface( bool server ) { @@ -99,7 +99,7 @@ namespace Steamworks public class SteamClientClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { @@ -122,7 +122,7 @@ namespace Steamworks public class SteamServerClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs index 1d02c3bf1..59d1517f9 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -48,12 +49,13 @@ namespace Steamworks internal IntPtr ptr; #pragma warning restore 649 - public unsafe static implicit operator string( Utf8StringPointer p ) + [return: NotNullIfNotNull("p")] + public unsafe static implicit operator string?( Utf8StringPointer p ) { - return ConvertPtrToString(p.ptr); + return ConvertPtrToString(p.ptr)!; } - public unsafe static string ConvertPtrToString(IntPtr ptr) + public unsafe static string? ConvertPtrToString(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; diff --git a/Libraries/Facepunch.Steamworks/Utility/Utility.cs b/Libraries/Facepunch.Steamworks/Utility/Utility.cs index 3365d2ff4..ad77d63d2 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utility.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utility.cs @@ -10,7 +10,7 @@ namespace Steamworks { public static partial class Utility { - static internal T ToType( this IntPtr ptr ) + static internal T? ToType( this IntPtr ptr ) { if ( ptr == IntPtr.Zero ) return default; @@ -18,7 +18,7 @@ namespace Steamworks return (T)Marshal.PtrToStructure( ptr, typeof( T ) ); } - static internal object ToType( this IntPtr ptr, System.Type t ) + static internal object? ToType( this IntPtr ptr, System.Type t ) { if ( ptr == IntPtr.Zero ) return default;