diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index e8d9f99f1..db7c27941 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -6,7 +6,7 @@ using System; namespace Barotrauma { - public class Camera + public class Camera : IDisposable { public static bool FollowSub = true; @@ -147,15 +147,19 @@ namespace Barotrauma position = Vector2.Zero; CreateMatrices(); - // TODO: Needs to unregister if ever destroy cameras. + // TODO: this has the potential to cause a resource leak + // by sneakily creating a reference to cameras that we might + // fail to release. GameMain.Instance.ResolutionChanged += CreateMatrices; UpdateTransform(false); } - ~Camera() + private bool disposed = false; + public void Dispose() { - GameMain.Instance.ResolutionChanged -= CreateMatrices; + if (!disposed) { GameMain.Instance.ResolutionChanged -= CreateMatrices; } + disposed = true; } public Vector2 TargetPos { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0891e5008..3c015ee76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -400,7 +400,7 @@ namespace Barotrauma partial void UpdateControlled(float deltaTime, Camera cam) { - if (controlled != this) return; + if (controlled != this) { return; } ControlLocalPlayer(deltaTime, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index b6304d486..02e09c587 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -3,7 +3,6 @@ using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index d96f35a87..305031f47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -266,7 +266,7 @@ namespace Barotrauma disguisedSkinColor = idCard.StoredOwnerAppearance.SkinColor; } - partial void LoadAttachmentSprites(bool omitJob) + partial void LoadAttachmentSprites() { if (attachmentSprites == null) { @@ -280,14 +280,6 @@ namespace Barotrauma Head.BeardElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); Head.HairElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); - if (omitJob) - { - JobPrefab.NoJobElement?.GetChildElement("PortraitClothing")?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); - } - else - { - Job?.Prefab.ClothingElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); - } } // Doesn't work if the head's source rect does not start at 0,0. @@ -988,11 +980,6 @@ namespace Barotrauma HeadSelectionList = null; } } - - ~AppearanceCustomizationMenu() - { - Dispose(); - } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index ddf2b6bc6..c78789db0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -551,6 +551,7 @@ namespace Barotrauma { bool hasOwner = inc.ReadBoolean(); int ownerId = hasOwner ? inc.ReadByte() : -1; + float humanPrefabHealthMultiplier = inc.ReadSingle(); int balance = inc.ReadInt32(); int rewardDistribution = inc.ReadRangedInteger(0, 100); byte teamID = inc.ReadByte(); @@ -573,6 +574,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) @@ -649,6 +651,8 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; + GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false; + character.memInput.Clear(); character.memState.Clear(); character.memLocalState.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 436e47141..5c563220d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -1345,6 +1344,7 @@ namespace Barotrauma { UserData = item, DisabledColor = Color.White * 0.1f, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } @@ -1352,6 +1352,7 @@ namespace Barotrauma if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } }; @@ -2008,8 +2009,27 @@ namespace Barotrauma DisplayedVitality = Vitality; } + partial void UpdateSkinTint() + { + if (!Character.IsVisible) { return; } + FaceTint = DefaultFaceTint; + BodyTint = Color.TransparentBlack; + + if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } + + foreach (KeyValuePair kvp in afflictions) + { + var affliction = kvp.Key; + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } + } + } + partial void UpdateLimbAfflictionOverlays() { + if (!Character.IsVisible) { return; } foreach (Limb limb in Character.AnimController.Limbs) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 19640dbc7..1ac28a552 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -58,21 +58,19 @@ namespace Barotrauma public class OutfitPreview { - /// - /// Pair.First = sprite, Pair.Second = draw offset - /// - public readonly List> Sprites; + public readonly List<(Sprite sprite, Vector2 drawOffset)> Sprites; + public Vector2 Dimensions; public OutfitPreview() { - Sprites = new List>(); + Sprites = new List<(Sprite sprite, Vector2 drawOffset)>(); Dimensions = Vector2.One; } public void AddSprite(Sprite sprite, Vector2 drawOffset) { - Sprites.Add(new Pair(sprite, drawOffset)); + Sprites.Add((sprite, drawOffset)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 41d2d470e..7fbcef8fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -224,14 +224,14 @@ namespace Barotrauma public float DamageOverlayStrength { get { return damageOverlayStrength; } - set { damageOverlayStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { damageOverlayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } private float burnOverLayStrength; public float BurnOverlayStrength { get { return burnOverLayStrength; } - set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } public string HitSoundTag => Params?.Sound?.Tag; @@ -279,7 +279,7 @@ namespace Barotrauma for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) { var param = Params.decorativeSpriteParams[i]; - var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param)); + var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param, ref _texturePath)); DecorativeSprites.Add(decorativeSprite); int groupID = decorativeSprite.RandomGroupID; if (!DecorativeSpriteGroups.ContainsKey(groupID)) @@ -295,13 +295,13 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams)); + Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath)); break; - case "damagedsprite": - DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); + case "damagedsprite": + DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath)); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null)); + var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath)); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -311,7 +311,7 @@ namespace Barotrauma } break; case "deformablesprite": - _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); + _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath)); var deformations = CreateDeformations(subElement); Deformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations); @@ -435,33 +435,33 @@ namespace Barotrauma { Sprite.Remove(); var source = Sprite.SourceElement; - Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams)); + Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams, ref _texturePath)); } if (_deformSprite != null) { _deformSprite.Remove(); var source = _deformSprite.Sprite.SourceElement; - _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams)); + _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams, ref _texturePath)); } if (DamagedSprite != null) { DamagedSprite.Remove(); var source = DamagedSprite.SourceElement; - DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams)); + DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams, ref _damagedTexturePath)); } for (int i = 0; i < ConditionalSprites.Count; i++) { var conditionalSprite = ConditionalSprites[i]; var source = conditionalSprite.ActiveSprite.SourceElement; conditionalSprite.Remove(); - ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null)); + ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null, ref _texturePath)); } for (int i = 0; i < DecorativeSprites.Count; i++) { var decorativeSprite = DecorativeSprites[i]; decorativeSprite.Remove(); var source = decorativeSprite.Sprite.SourceElement; - DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i])); + DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i], ref _texturePath)); } } @@ -472,16 +472,17 @@ namespace Barotrauma } private string _texturePath; - private string GetSpritePath(ContentXElement element, SpriteParams spriteParams) + private string _damagedTexturePath; + private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, ref string path) { - if (_texturePath == null) + if (path == null) { if (spriteParams != null) { ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) ?? ContentPath.FromRaw(character.Prefab.ContentPackage, spriteParams.GetTexturePath()); - _texturePath = GetSpritePath(texturePath); + path = GetSpritePath(texturePath); } else { @@ -489,10 +490,10 @@ namespace Barotrauma texturePath = texturePath.IsNullOrWhiteSpace() ? ContentPath.FromRaw(character.Prefab.ContentPackage, ragdoll.RagdollParams.Texture) : texturePath; - _texturePath = GetSpritePath(texturePath); + path = GetSpritePath(texturePath); } } - return _texturePath; + return path; } /// @@ -625,12 +626,7 @@ namespace Barotrauma { if (!body.Enabled) { return; } - if (!IsDead) - { - DamageOverlayStrength -= deltaTime; - BurnOverlayStrength -= deltaTime; - } - else + if (IsDead) { var spriteParams = Params.GetSprite(); if (spriteParams != null && spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) @@ -688,7 +684,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { - float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; + float brightness = Math.Max(1.0f - burnOverLayStrength, 0.2f); var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } @@ -831,32 +827,6 @@ namespace Barotrauma { LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } - if (damageOverlayStrength > 0.0f && DamagedSprite != null && !hideLimb) - { - DamagedSprite.Draw(spriteBatch, - new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - color * Math.Min(damageOverlayStrength, 1.0f), activeSprite.Origin, - -body.DrawRotation, - Scale, spriteEffect, activeSprite.Depth - (depthStep * 90)); - } - foreach (var decorativeSprite in DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); - if (deadTimer > 0) - { - c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); - } - c = overrideColor ?? c; - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; - var ca = (float)Math.Cos(-body.Rotation); - var sa = (float)Math.Sin(-body.Rotation); - Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, - -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, - depth: activeSprite.Depth - (depthStep * 100)); - } float step = depthStep; WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) @@ -925,6 +895,36 @@ namespace Barotrauma //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } + if (!Hide && onlyDrawable == null) + { + foreach (var decorativeSprite in DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); + if (deadTimer > 0) + { + c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); + } + c = overrideColor ?? c; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; + var ca = (float)Math.Cos(-body.Rotation); + var sa = (float)Math.Sin(-body.Rotation); + Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, + -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, + depth: activeSprite.Depth - depthStep); + depthStep += step; + } + if (damageOverlayStrength > 0.0f && DamagedSprite != null) + { + DamagedSprite.Draw(spriteBatch, + new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), + color * damageOverlayStrength, activeSprite.Origin, + -body.DrawRotation, + Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos + } + } if (GameMain.DebugDraw) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 166979300..088fb2725 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -108,6 +108,15 @@ namespace Barotrauma } } + public void RemoveFile(File file) + { + if (HasFile(file)) + { + files.Remove(file); + DiscardHashAndInstallTime(); + } + } + public void DiscardHashAndInstallTime() { ExpectedHash = null; @@ -144,7 +153,7 @@ namespace Barotrauma => rootElement.Add(new XAttribute(name, value.ToString() ?? "")); addRootAttribute("name", Name); - addRootAttribute("modversion", ModVersion); + if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } addRootAttribute("corepackage", IsCore); if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } addRootAttribute("gameversion", GameMain.Version); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs new file mode 100644 index 000000000..43fe28e83 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs @@ -0,0 +1,133 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Steam; +using Barotrauma.IO; + +namespace Barotrauma +{ + public static class ModMerger + { + public static void AskMerge(ContentPackage[] mods) + { + ErrorIfNonLocal(mods); + + var msgBox = new GUIMessageBox(TextManager.Get("MergeModsHeader"), "", relativeSize: (0.5f, 0.8f), + buttons: new LocalizedString[] { TextManager.Get("ConfirmModMerge"), TextManager.Get("Cancel") }); + msgBox.Buttons[1].OnClicked = msgBox.Close; + + var desc = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsDesc")); + var modsList = new GUIListBox(new RectTransform((1.0f, 0.5f), msgBox.Content.RectTransform)) + { + OnSelected = (component, o) => false, + HoverCursor = CursorState.Default + }; + foreach (var mod in mods) + { + new GUITextBlock(new RectTransform((1.0f, 0.11f), modsList.Content.RectTransform), mod.Name) + { + CanBeFocused = false + }; + } + var footer = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsFooter")); + var resultName = new GUITextBox(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform)) + { + Text = (mods.Count(m => m.Files.Length > 1)==1) + ? mods.First(m => m.Files.Length > 1).Name + : "" + }; + + void flashText() + { + resultName!.Select(); + resultName.Flash(GUIStyle.Red); + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (string.IsNullOrEmpty(resultName.Text)) + { + flashText(); + return false; + } + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName.Text}"; + + bool dirMatches(ContentPackage mod) + => mod.Dir.CleanUpPathCrossPlatform(correctFilenameCase: false) + .Equals(targetDir, StringComparison.OrdinalIgnoreCase); + if (ContentPackageManager.LocalPackages.Any(dirMatches) + && !mods.Any(dirMatches)) + { + flashText(); + return false; + } + + MergeMods(mods, resultName.Text); + msgBox.Close(); + return false; + }; + } + + private static void MergeMods(ContentPackage[] mods, string resultName) + { + ModProject resultProject = new ModProject + { + Name = resultName + }; + + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName}"; + Directory.CreateDirectory(targetDir); + + foreach (var mod in mods) + { + foreach (var file in Directory.GetFiles(mod.Dir, "*", System.IO.SearchOption.AllDirectories) + .Select(f => f.CleanUpPathCrossPlatform(correctFilenameCase: false))) + { + if (Path.GetFileName(file).Equals(ContentPackage.FileListFileName, StringComparison.OrdinalIgnoreCase)) { continue; } + + string targetFilePath = file[mod.Dir.Length..]; + if (targetFilePath.StartsWith("/") || targetFilePath.StartsWith("\\")) + { + targetFilePath = targetFilePath[1..]; + } + + targetFilePath = Path.Combine(targetDir, targetFilePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + //DebugConsole.NewMessage(targetFilePath); + + Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!); + File.Copy(file, targetFilePath, overwrite: true); + + var oldFileInProject = resultProject.Files.FirstOrDefault(f + => f.Path.Equals(targetFilePath, StringComparison.OrdinalIgnoreCase)); + if (oldFileInProject != null) + { + resultProject.RemoveFile(oldFileInProject); + } + + var fileInMod = mod.Files.Find(f => f.Path == file); + if (fileInMod != null) + { + var newFileInProject = ModProject.File.FromPath(targetFilePath, fileInMod.GetType()); + resultProject.AddFile(newFileInProject); + } + } + } + resultProject.Save(Path.Combine(targetDir, ContentPackage.FileListFileName)); + + foreach (var mod in mods) + { + Directory.Delete(mod.Dir); + } + (SettingsMenu.Instance!.WorkshopMenu as MutableWorkshopMenu)!.PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); + } + + private static void ErrorIfNonLocal(ContentPackage[] mods) + { + var nonLocal = mods.Where(m => !ContentPackageManager.LocalPackages.Contains(m)).ToArray(); + if (nonLocal.Any()) + { + throw new Exception($"{string.Join(", ", nonLocal.Select(m => m.Name))} are not local mods"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs index eb85f6b64..748995b91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,9 +8,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Steam; using Microsoft.Xna.Framework; -using Directory = Barotrauma.IO.Directory; -using File = Barotrauma.IO.File; -using Path = Barotrauma.IO.Path; +using Barotrauma.IO; namespace Barotrauma.Transition { @@ -258,13 +255,13 @@ namespace Barotrauma.Transition { string[] getFiles(string path, string pattern) => Directory.Exists(path) - ? Directory.GetFiles(path, pattern, SearchOption.TopDirectoryOnly) + ? Directory.GetFiles(path, pattern, System.IO.SearchOption.TopDirectoryOnly) : Array.Empty(); subs = getFiles(oldSubsPath, "*.sub"); itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml"); - string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", SearchOption.TopDirectoryOnly); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly); var publishedItems = await SteamManager.Workshop.GetPublishedItems(); foreach (var modDir in allOldMods) @@ -359,7 +356,7 @@ namespace Barotrauma.Transition else { //copying a mod: we have a neat method for that! - await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath); + await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath, SteamManager.Workshop.ShouldCorrectPaths.Yes); return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index a13d82f7f..b8cff8d92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -125,7 +125,7 @@ namespace Barotrauma { if (isOpen) { - frame.AddToGUIUpdateList(); + frame.AddToGUIUpdateList(order: 1); } } @@ -172,6 +172,7 @@ namespace Barotrauma isOpen = false; GUI.ForceMouseOn(null); textBox.Deselect(); + SoundPlayer.PlayUISound(GUISoundType.Select); } if (isOpen) @@ -209,7 +210,7 @@ namespace Barotrauma isOpen = !isOpen; if (isOpen) { - textBox.Select(); + textBox.Select(ignoreSelectSound: true); AddToGUIUpdateList(); } else @@ -217,6 +218,7 @@ namespace Barotrauma GUI.ForceMouseOn(null); textBox.Deselect(); } + SoundPlayer.PlayUISound(GUISoundType.Select); } private static bool IsCommandPermitted(string command, GameClient client) @@ -1714,9 +1716,47 @@ namespace Barotrauma //check missing mission texts foreach (var missionPrefab in MissionPrefab.Prefabs) { - Identifier missionId = (missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); - addIfMissing($"missionname.{missionId}".ToIdentifier(), language); - addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + Identifier missionId = missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ? + missionPrefab.Identifier : + missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty); + + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("name", Identifier.Empty))) + { + addIfMissing($"missionname.{missionId}".ToIdentifier(), language); + } + + if (missionPrefab.Type == MissionType.Combat) + { + addIfMissing($"MissionDescriptionNeutral.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionDescription1.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionDescription2.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionTeam1.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionTeam2.{missionId}".ToIdentifier(), language); + } + else + { + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("description", Identifier.Empty))) + { + addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + } + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("successmessage", Identifier.Empty))) + { + addIfMissing($"missionsuccess.{missionId}".ToIdentifier(), language); + } + //only check failure message if there's something defined in the xml (otherwise we just use the generic "missionfailed" text) + if (missionPrefab.ConfigElement.GetAttribute("failuremessage") != null && + !tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("failuremessage", Identifier.Empty))) + { + addIfMissing($"missionfailure.{missionId}".ToIdentifier(), language); + } + } + for (int i = 0; i type.IsSubclassOf(typeof(ItemComponent)))) @@ -2503,7 +2543,7 @@ namespace Barotrauma var entity = MapEntity.mapEntityList[i] as ISerializableEntity; if (entity != null) { - List> allProperties = new List>(); + List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); if (entity is Item item) { @@ -2518,14 +2558,14 @@ namespace Barotrauma for (int k = 0; k < properties.Count; k++) { - allProperties.Add(new Pair(entity, properties[k])); + allProperties.Add((entity, properties[k])); } } for (int j = 0; j < allProperties.Count; j++) { - var property = allProperties[j].Second; - string propertyName = (allProperties[j].First.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); + var property = allProperties[j].property; + string propertyName = (allProperties[j].obj.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); LocalizedString displayName = TextManager.Get($"sp.{propertyName}.name"); if (displayName.IsNullOrEmpty()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 7f8c08485..d9dd3a9cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -308,7 +308,7 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.Get(text); + LocalizedString translatedText = TextManager.Get(text).Fallback(text); if (speaker?.Info != null && drawChathead) { @@ -335,7 +335,7 @@ namespace Barotrauma { foreach (string option in options) { - var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option), style: "ListBoxElement"); + var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option).Fallback(option), style: "ListBoxElement"); btn.TextBlock.TextAlignment = Alignment.CenterLeft; btn.TextColor = btn.HoverTextColor = GUIStyle.Green; btn.TextBlock.Wrap = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs index dbfd01ecc..f4a737254 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs @@ -20,14 +20,12 @@ namespace Barotrauma if (targetId == Entity.NullEntityID) { continue; } Entity target = Entity.FindEntityByID(targetId); if (target == null) { continue; } - existingTargets.Add(target); allTargets.Add(target); } ushort spawnedTargetsCount = msg.ReadUInt16(); for (int i = 0; i < spawnedTargetsCount; i++) { var enemy = Character.ReadSpawnData(msg); - existingTargets.Add(enemy); allTargets.Add(enemy); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index c1ee89fb8..114d62cdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -81,6 +81,8 @@ namespace Barotrauma public const int ToggleButtonWidthRaw = 30; private int popupMessageOffset; + private GUIDropDown ChatModeDropDown { get; set; } + public ChatBox(GUIComponent parent, bool isSinglePlayer) { this.IsSinglePlayer = isSinglePlayer; @@ -107,6 +109,7 @@ namespace Barotrauma var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -150,6 +153,7 @@ namespace Barotrauma var buttonRight = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -178,6 +182,7 @@ namespace Barotrauma TextColor = new Color(51, 59, 46), SelectedTextColor = GUIStyle.Green, UserData = i, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -223,7 +228,53 @@ namespace Barotrauma // --------------------------------------------------------------------------------------------- - InputBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.125f), hideableElements.RectTransform, Anchor.BottomLeft), + var bottomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.125f), hideableElements.RectTransform, Anchor.BottomLeft), isHorizontal: true) + { + Stretch = true + }; + + var dropdownRt = new RectTransform(new Vector2(0.1f, 1.0f), bottomContainer.RectTransform) + { + // The chat mode selection dropdown will take a maximum of 45% of the horizontal space + MaxSize = new Point((int)(0.45f * bottomContainer.RectTransform.NonScaledSize.X), int.MaxValue) + }; + var chatModes = new ChatMode[] { ChatMode.Local, ChatMode.Radio }; + ChatModeDropDown = new GUIDropDown(dropdownRt, elementCount: chatModes.Length, dropAbove: true) + { + OnSelected = (component, userdata) => + { + GameMain.ActiveChatMode = (ChatMode)userdata; + if (InputBox != null && InputBox.Text.StartsWith(RadioChatString) && GameMain.ActiveChatMode == ChatMode.Local) + { + string text = InputBox.Text; + InputBox.Text = text.Remove(0, RadioChatString.Length); + } + return true; + } + }; + float longestDropDownOption = 0.0f; + foreach (ChatMode mode in chatModes) + { + var text = TextManager.Get($"chatmode.{mode}"); + ChatModeDropDown.AddItem(text, userData: mode); + if (ChatModeDropDown.ListBox.Content.GetChildByUserData(mode) is GUITextBlock textBlock) + { + if (textBlock.TextSize.X > longestDropDownOption) + { + longestDropDownOption = textBlock.TextSize.X; + } + } + } + ChatModeDropDown.SelectItem(GameMain.ActiveChatMode); + + float minDropDownWidth = longestDropDownOption + ChatModeDropDown.Padding.X + + (ChatModeDropDown.DropDownIcon?.RectTransform.NonScaledSize.X ?? 0) + + (ChatModeDropDown.DropDownIcon?.RectTransform.AbsoluteOffset.X ?? 0) * 2; + ChatModeDropDown.RectTransform.MinSize = new Point( + Math.Max((int)minDropDownWidth, ChatModeDropDown.RectTransform.MinSize.X), + ChatModeDropDown.RectTransform.MinSize.Y); + + InputBox = new GUITextBox(new RectTransform(new Vector2(0.9f, 1.0f), bottomContainer.RectTransform), style: "ChatTextBox") { OverflowClip = true, @@ -236,6 +287,11 @@ namespace Barotrauma InputBox.OnDeselected += (gui, Keys) => { ChatManager.Clear(); + if (GUIFrame.IsParentOf(GUI.MouseOn)) + { + CloseAfterMessageSent = false; + return; + } ChatMessage.GetChatMessageCommand(InputBox.Text, out var message); if (string.IsNullOrEmpty(message)) { @@ -245,8 +301,6 @@ namespace Barotrauma CloseAfterMessageSent = false; } } - - //gui.Text = ""; }; var chatSendButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.7f), InputBox.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleRight"); @@ -303,6 +357,10 @@ namespace Barotrauma { textColor = ChatMessage.MessageColor[(int)ChatMessageType.Private]; } + else if (GameMain.ActiveChatMode == ChatMode.Radio) + { + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Radio]; + } else { textColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; @@ -357,10 +415,15 @@ namespace Barotrauma CanBeFocused = true, ForceUpperCase = ForceUpperCase.No, UserData = message.SenderClient, + PlaySoundOnSelect = false, OnClicked = (_, o) => { if (!(o is Client client)) { return false; } - GameMain.NetLobbyScreen?.SelectPlayer(client); + if (GameMain.NetLobbyScreen != null) + { + GameMain.NetLobbyScreen.SelectPlayer(client); + SoundPlayer.PlayUISound(GUISoundType.Select); + } return true; }, OnSecondaryClicked = (_, o) => @@ -542,6 +605,25 @@ namespace Barotrauma showNewMessagesButton.Visible = false; } + if (PlayerInput.KeyHit(InputType.ToggleChatMode) && GUI.KeyboardDispatcher.Subscriber == null && Screen.Selected == GameMain.GameScreen) + { + try + { + var mode = GameMain.ActiveChatMode switch + { + ChatMode.Local => ChatMode.Radio, + ChatMode.Radio => ChatMode.Local, + _ => throw new NotImplementedException() + }; + ChatModeDropDown.SelectItem(mode); + // TODO: Play a sound? + } + catch (NotImplementedException) + { + DebugConsole.ThrowError($"Error toggling chat mode: not implemented for current mode \"{GameMain.ActiveChatMode}\""); + } + } + if (ToggleButton != null) { ToggleButton.Selected = ToggleOpen; @@ -692,5 +774,70 @@ namespace Barotrauma } } } + + public void ApplySelectionInputs() => ApplySelectionInputs(InputBox, true, ChatKeyStates.GetChatKeyStates()); + + public struct ChatKeyStates + { + public bool ActiveChatKeyHit { get; set; } + public bool LocalChatKeyHit { get; set; } + public bool RadioChatKeyHit { get; set; } + public bool AnyHit => ActiveChatKeyHit || LocalChatKeyHit || RadioChatKeyHit; + + private ChatKeyStates(bool active, bool local, bool radio) + { + ActiveChatKeyHit = active; + LocalChatKeyHit = local; + RadioChatKeyHit = radio; + } + + public static ChatKeyStates GetChatKeyStates() + { + return new ChatKeyStates(PlayerInput.KeyHit(InputType.ActiveChat), + PlayerInput.KeyHit(InputType.Chat), + PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100)); + } + + public (bool active, bool local, bool radio) Deconstruct() + { + return (ActiveChatKeyHit, LocalChatKeyHit, RadioChatKeyHit); + } + } + + public void ApplySelectionInputs(GUITextBox inputBox, bool selectInputBox, ChatKeyStates chatKeyStates) + { + inputBox ??= InputBox; + var (activeChatKeyHit, localChatKeyHit, radioChatKeyHit) = chatKeyStates.Deconstruct(); + if (localChatKeyHit || (activeChatKeyHit && GameMain.ActiveChatMode == ChatMode.Local)) + { + ChatModeDropDown.SelectItem(ChatMode.Local); + inputBox.AddToGUIUpdateList(); + GUIFrame.Flash(Color.DarkGreen, 0.5f); + if (!ToggleOpen) + { + CloseAfterMessageSent = !ToggleOpen; + ToggleOpen = true; + } + if (selectInputBox) + { + inputBox.Select(inputBox.Text.Length); + } + } + else if (radioChatKeyHit || (activeChatKeyHit && GameMain.ActiveChatMode == ChatMode.Radio)) + { + ChatModeDropDown.SelectItem(ChatMode.Radio); + inputBox.AddToGUIUpdateList(); + GUIFrame.Flash(Color.YellowGreen, 0.5f); + if (!ToggleOpen) + { + CloseAfterMessageSent = !ToggleOpen; + ToggleOpen = true; + } + if (selectInputBox) + { + inputBox.Select(inputBox.Text.Length); + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index e99e15745..9a33f0ce9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -178,7 +178,18 @@ namespace Barotrauma return Sprites.ContainsKey(state) ? Sprites[state]?.First()?.Sprite : null; } - public void GetSize(XElement element) + public void RefreshSize() + { + Width = null; + Height = null; + GetSize(Element); + foreach (var childStyle in ChildStyles.Values) + { + childStyle.RefreshSize(); + } + } + + private void GetSize(XElement element) { Point size = new Point(0, 0); foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 43b772d1c..3aa3511df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -193,12 +193,13 @@ namespace Barotrauma }; validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { - ClickSound = GUISoundType.HireRepairClick, + ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() @@ -403,6 +404,7 @@ namespace Barotrauma { var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) @@ -429,6 +431,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 71023ab04..4fcd29809 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -182,7 +182,10 @@ namespace Barotrauma window = new GUIFrame(new RectTransform(Vector2.One * 0.8f, backgroundFrame.RectTransform, Anchor.Center)); var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true); - sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)); + sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)) + { + PlaySoundOnSelect = true + }; var drives = System.IO.DriveInfo.GetDrives(); foreach (var drive in drives) @@ -241,6 +244,7 @@ namespace Barotrauma fileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), fileListLayout.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (child, userdata) => { if (userdata is null) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 4b2e186a8..49f027c19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -24,15 +24,17 @@ namespace Barotrauma ChatMessage, RadioMessage, DeadMessage, - Click, + Select, PickItem, PickItemFail, DropItem, PopupMenu, - DecreaseQuantity, - IncreaseQuantity, - HireRepairClick, - UISwitch + Decrease, + Increase, + UISwitch, + TickBox, + ConfirmTransaction, + Cart, } public enum CursorState @@ -301,6 +303,7 @@ namespace Barotrauma } float startY = 10.0f; + float yStep = AdjustForTextScale(18) * yScale; if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf) { float y = startY; @@ -309,11 +312,38 @@ namespace Barotrauma Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { - y += AdjustForTextScale(15) * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), $"Physics: {GameMain.CurrentUpdateRate}", (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } + if (GameMain.DebugDraw || GameMain.ShowPerf) + { + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Active lights: " + Lights.LightManager.ActiveLightCount, + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Physics: " + GameMain.World.UpdateTime.TotalMilliseconds + " ms", + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + try + { + DrawString(spriteBatch, new Vector2(10, y), + $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + } + catch (InvalidOperationException) + { + DebugConsole.AddWarning("Exception while rendering debug info. Physics bodies may have been created or removed while rendering."); + } + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, + Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + + } } if (GameMain.ShowPerf) @@ -324,67 +354,53 @@ namespace Barotrauma "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: GUIStyle.Green); - y += 50 * yScale; + y += yStep * 4; DrawString(spriteBatch, new Vector2(x, y), "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms", Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: Color.LightBlue); - y += 50 * yScale; - foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) + y += yStep * 4; + foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers.OrderBy(i => i)) { float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); - DrawString(spriteBatch, new Vector2(x, y), - key + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; - foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) - { - elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); - DrawString(spriteBatch, new Vector2(x + 15, y), - childKey + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; - } - } + int categoryDepth = key.Count(c => c == ':'); + //color the more fine-grained counters red more easily (ok for the whole Update to take a longer time than specific part of the update) + float runningSlowThreshold = 10.0f / categoryDepth; + DrawString(spriteBatch, new Vector2(x + categoryDepth * 15, y), + key.Split(':').Last() + ": " + elapsedMillisecs.ToString("0.00"), + ToolBox.GradientLerp(elapsedMillisecs / runningSlowThreshold, Color.LightGreen, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red, Color.Magenta), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + } if (Powered.Grids != null) { DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; } - if (Settings.EnableDiagnostics) { - x += 20 * xScale; - DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 15 * yScale), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 30 * yScale), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 45 * yScale), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 60 * yScale), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 75 * yScale), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + x += yStep * 2; + DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen)) { - float y = startY + 15 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - "Physics: " + GameMain.World.UpdateTime, - Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - - y += 15 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", - Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + float y = startY + yStep * 6; if (Screen.Selected.Cam != null) { - y += 15 * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); @@ -392,23 +408,18 @@ namespace Barotrauma if (Submarine.MainSub != null) { - y += 15 * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), "Sub pos: " + Submarine.MainSub.Position.ToPoint(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } - y += 20 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, - Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime) { loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)"; loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5); } - y += 25 * yScale; + y += yStep * 2; DrawString(spriteBatch, new Vector2(10, y), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (debugDrawSounds) @@ -416,21 +427,21 @@ namespace Barotrauma float soundTextY = 0; DrawString(spriteBatch, new Vector2(500, soundTextY), "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) { @@ -479,7 +490,7 @@ namespace Barotrauma } DrawString(spriteBatch, new Vector2(500, soundTextY), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; } } else @@ -1981,7 +1992,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = font }; @@ -2025,7 +2036,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -2055,7 +2066,7 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { Font = font }; + GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Float) { Font = font }; switch (i) { case 0: @@ -2369,7 +2380,7 @@ namespace Barotrauma CreateButton("PauseMenuResume", buttonContainer, null); CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true); - bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; + bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) @@ -2384,11 +2395,11 @@ namespace Barotrauma GameMain.GameSession.LoadPreviousSave(); }); - if (IsOutpostLevel()) + if (IsFriendlyOutpostLevel()) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { - if (IsOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } + if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } }); } } @@ -2401,7 +2412,7 @@ namespace Barotrauma } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { - bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsOutpostLevel(); + bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel(); if (canSave) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 1846bc809..a824218e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -111,6 +111,11 @@ namespace Barotrauma set { textBlock.SelectedTextColor = value; } } + public Color DisabledTextColor + { + get { return textBlock.DisabledTextColor; } + } + public override float FlashTimer { get { return Frame.FlashTimer; } @@ -159,7 +164,9 @@ namespace Barotrauma private float pulseExpand; private bool flashed; - public GUISoundType ClickSound { get; set; } = GUISoundType.Click; + public GUISoundType ClickSound { get; set; } = GUISoundType.Select; + + public override bool PlaySoundOnSelect { get; set; } = true; public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } @@ -247,7 +254,10 @@ namespace Barotrauma } else if (PlayerInput.PrimaryMouseButtonClicked()) { - SoundPlayer.PlayUISound(ClickSound); + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(ClickSound); + } if (OnClicked != null) { if (OnClicked(this, UserData)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index c555ce055..d27e2c086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,6 +8,8 @@ namespace Barotrauma { public class GUICanvas : RectTransform { + private static readonly object mutex = new object(); + protected GUICanvas() : base(size, parent: null) { } private static GUICanvas _instance; @@ -39,22 +41,25 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + lock (mutex) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - _instance.childrenWeakRef.Add(new WeakReference(child)); + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + { + _instance.childrenWeakRef.Add(new WeakReference(child)); + } } - } - //get rid of strong references - _instance.children.Clear(); - //remove dead children - for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) - { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) { - _instance.childrenWeakRef.RemoveAt(i); + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + { + _instance.childrenWeakRef.RemoveAt(i); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index deb5dab1c..dff86c500 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -383,6 +383,8 @@ namespace Barotrauma public bool ExternalHighlight = false; + public virtual bool PlaySoundOnSelect { get; set; } = false; + private RectTransform rectTransform; public RectTransform RectTransform { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index eeebb32c4..40eae2afa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -113,7 +113,8 @@ namespace Barotrauma { AutoHideScrollBar = false, ScrollBarVisible = false, - Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding + Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding, + PlaySoundOnSelect = true }; foreach (var (option, size) in optionsAndSizes) @@ -290,7 +291,7 @@ namespace Barotrauma public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { base.AddToGUIUpdateList(ignoreChildren, order); - SubMenu?.AddToGUIUpdateList(); + SubMenu?.AddToGUIUpdateList(order: 2); } public static void AddActiveToGUIUpdateList() @@ -300,7 +301,7 @@ namespace Barotrauma CurrentContextMenu = null; } - CurrentContextMenu?.AddToGUIUpdateList(); + CurrentContextMenu?.AddToGUIUpdateList(order: 2); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index d34f6bf91..f8e908d8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -160,6 +160,10 @@ namespace Barotrauma listBox.ToolTip = value; } } + + public GUIImage DropDownIcon => icon; + + public Vector4 Padding => button.TextBlock.Padding; public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) { @@ -183,7 +187,8 @@ namespace Barotrauma listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) { IsFixedSize = false }, style: null) { - Enabled = !selectMultiple + Enabled = !selectMultiple, + PlaySoundOnSelect = true, }; if (!selectMultiple) { listBox.OnSelected = SelectItem; } GUIStyle.Apply(listBox, "GUIListBox", this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 0089c3e94..8e43f9c6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -309,6 +309,45 @@ namespace Barotrauma } } + public override bool PlaySoundOnSelect { get; set; } = false; + + public bool PlaySoundOnDragStop { get; set; } = false; + + public GUISoundType? SoundOnDragStart { get; set; } = null; + + public GUISoundType? SoundOnDragStop { get; set; } = null; + + #region enums + public enum Force + { + Yes, + No + } + + public enum AutoScroll + { + Enabled, + Disabled + } + + public enum TakeKeyBoardFocus + { + Yes, + No + } + + public enum PlaySelectSound + { + Yes, + No + } + + private AutoScroll GetAutoScroll(bool b) + { + return b ? AutoScroll.Enabled : AutoScroll.Disabled; + } + #endregion + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { @@ -396,7 +435,7 @@ namespace Barotrauma UpdateScrollBarSize(); } - public void Select(object userData, bool force = false, bool autoScroll = true) + public void Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled) { var children = Content.Children; int i = 0; @@ -515,9 +554,12 @@ namespace Barotrauma /// Scrolls the list to the specific element. /// /// - public void ScrollToElement(GUIComponent component, bool playSound = true) + public void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound = PlaySelectSound.No) { - if (playSound) { SoundPlayer.PlayUISound(GUISoundType.Click); } + if (playSelectSound == PlaySelectSound.Yes) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } @@ -573,9 +615,16 @@ namespace Barotrauma } } + private double lastDragStartTime; + private void StartDraggingElement(GUIComponent child) { DraggedElement = child; + if (Timing.TotalTime > lastDragStartTime + 0.2f) + { + lastDragStartTime = Timing.TotalTime; + SoundPlayer.PlayUISound(SoundOnDragStart); + } } private bool UpdateDragging() @@ -586,6 +635,10 @@ namespace Barotrauma var draggedElem = draggedElement; OnRearranged?.Invoke(this, draggedElem.UserData); DraggedElement = null; + if (PlaySoundOnDragStop) + { + SoundPlayer.PlayUISound(SoundOnDragStop); + } RepositionChildren(); if (AllSelected.Contains(draggedElem)) { return true; } } @@ -710,7 +763,7 @@ namespace Barotrauma int index = Content.Children.ToList().IndexOf(component); if (index >= 0) { - Select(index, false, false, takeKeyBoardFocus: true); + Select(index, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes); } } } @@ -733,7 +786,7 @@ namespace Barotrauma { ScrollToElement(child); } - Select(i, autoScroll: false, takeKeyBoardFocus: true); + Select(i, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } if (CurrentDragMode != DragMode.NoDragging @@ -929,14 +982,13 @@ namespace Barotrauma if (ClampScrollToElements) { bool scrollDown = Math.Clamp(PlayerInput.ScrollWheelSpeed, 0, 1) > 0; - if (scrollDown) { - SelectPrevious(takeKeyBoardFocus: true); + SelectPrevious(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } else { - SelectNext(takeKeyBoardFocus: true); + SelectNext(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } } } @@ -964,7 +1016,7 @@ namespace Barotrauma return FindScrollableParentListBox(target.Parent); } - public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectNext(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex + 1; while (index < Content.CountChildren) @@ -972,10 +1024,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -983,7 +1035,7 @@ namespace Barotrauma } } - public void SelectPrevious(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectPrevious(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex - 1; while (index >= 0) @@ -991,10 +1043,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -1002,7 +1054,7 @@ namespace Barotrauma } } - public void Select(int childIndex, bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void Select(int childIndex, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { if (childIndex >= Content.CountChildren || childIndex < 0) { return; } @@ -1013,7 +1065,7 @@ namespace Barotrauma if (OnSelected != null) { // TODO: The callback is called twice, fix this! - wasSelected = force || OnSelected(child, child.UserData); + wasSelected = force == Force.Yes || OnSelected(child, child.UserData); } if (!wasSelected) { return; } @@ -1055,7 +1107,7 @@ namespace Barotrauma // Ensure that the selected element is visible. This may not be the case, if the selection is run from code. (e.g. if we have two list boxes that are synced) // TODO: This method only works when moving one item up/down (e.g. when using the up and down arrows) - if (autoScroll) + if (autoScroll == AutoScroll.Enabled) { if (ScrollBar.IsHorizontal) { @@ -1086,11 +1138,19 @@ namespace Barotrauma } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. - if (takeKeyBoardFocus && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) + if (takeKeyBoardFocus == TakeKeyBoardFocus.Yes && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; } + + // List box child components can be parents to other components that can play sounds when selected (e.g. store elements) + // so the list box shouldn't play the Select sound if the GUI.MouseOn component has a sound to play + if (playSelectSound == PlaySelectSound.Yes && PlaySoundOnSelect && !child.PlaySoundOnSelect && + (GUI.MouseOn == null || GUI.MouseOn.Parent == Content || !GUI.MouseOn.PlaySoundOnSelect)) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Select(IEnumerable children) @@ -1293,16 +1353,16 @@ namespace Barotrauma switch (key) { case Keys.Down: - if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Up: - if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Left: - if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Right: - if (isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Enter: case Keys.Space: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index b7265c76f..f3802cca6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -7,11 +7,6 @@ namespace Barotrauma { class GUINumberInput : GUIComponent { - public enum NumberType - { - Int, Float - } - public delegate void OnValueEnteredHandler(GUINumberInput numberInput); public OnValueEnteredHandler OnValueEntered; @@ -187,7 +182,7 @@ namespace Barotrauma public float valueStep; private float pressedTimer; - private float pressedDelay = 0.5f; + private readonly float pressedDelay = 0.5f; private bool IsPressedTimerRunning { get { return pressedTimer > 0; } } public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null, bool hidePlusMinusButtons = false) : base(style, rectT) @@ -233,6 +228,7 @@ namespace Barotrauma var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); GUIStyle.Apply(PlusButton, "PlusButton", this); + PlusButton.ClickSound = GUISoundType.Increase; PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -254,6 +250,7 @@ namespace Barotrauma MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); GUIStyle.Apply(MinusButton, "MinusButton", this); + MinusButton.ClickSound = GUISoundType.Decrease; MinusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -428,8 +425,8 @@ namespace Barotrauma intValue = Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = intValue < MaxValueInt; - MinusButton.Enabled = intValue > MinValueInt; + PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 1487d6943..137eee850 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -98,7 +98,6 @@ namespace Barotrauma foreach (var subElement in element.Elements().Reverse()) { if (subElement.NameAsIdentifier() != "override") { continue; } - if (subElement.GetAttributeBool("iscjk", false)) { return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); @@ -111,8 +110,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeContentPath("file")?.Value; } @@ -125,8 +123,7 @@ namespace Barotrauma //check if any of the language override fonts want to override the font size as well foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { uint overrideFontSize = GetFontSize(subElement, 0); if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.CurrentConfig.Graphics.TextScale); } @@ -149,8 +146,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("dynamicloading", false); } @@ -162,14 +158,20 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("iscjk", false); } } return element.GetAttributeBool("iscjk", false); } + + private bool IsValidOverride(XElement element) + { + if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; } + var languages = element.GetAttributeIdentifierArray("language", Array.Empty()); + return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language); + } } public class GUIFont : GUISelector diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 1a17d1124..30ec4af6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -322,9 +322,8 @@ namespace Barotrauma { if (!enabled || !PlayerInput.PrimaryMouseButtonDown()) { return false; } if (barSize >= 1.0f) { return false; } - DraggingBar = this; - + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 850f347bb..27be2752f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -34,7 +34,6 @@ namespace Barotrauma public readonly static PrefabCollection ComponentStyles = new PrefabCollection(); public readonly static GUIFont Font = new GUIFont("Font"); - public readonly static GUIFont GlobalFont = new GUIFont("GlobalFont"); public readonly static GUIFont UnscaledSmallFont = new GUIFont("UnscaledSmallFont"); public readonly static GUIFont SmallFont = new GUIFont("SmallFont"); public readonly static GUIFont LargeFont = new GUIFont("LargeFont"); @@ -142,10 +141,6 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public readonly static GUIColor EquipmentIndicatorNotEquipped = new GUIColor("EquipmentIndicatorNotEquipped"); - public readonly static GUIColor EquipmentIndicatorEquipped = new GUIColor("EquipmentIndicatorEquipped"); - public readonly static GUIColor EquipmentIndicatorRunningOut = new GUIColor("EquipmentIndicatorRunningOut"); - public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); @@ -159,7 +154,7 @@ namespace Barotrauma public static void Apply(GUIComponent targetComponent, Identifier styleName, GUIComponent parent = null) { - GUIComponentStyle componentStyle = null; + GUIComponentStyle componentStyle; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; @@ -212,5 +207,21 @@ namespace Barotrauma return ItemQualityColorNormal; } } + + public static void RecalculateFonts() + { + foreach (var font in Fonts.Values) + { + font.Prefabs.ForEach(p => p.LoadFont()); + } + } + + public static void RecalculateSizeRestrictions() + { + foreach (var componentStyle in ComponentStyles) + { + componentStyle.RefreshSize(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index e4c047bff..404c09927 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -35,7 +35,6 @@ namespace Barotrauma public TextGetterHandler TextGetter; public bool Wrap; - private bool playerInput; public bool RoundToNearestPixel = true; @@ -287,8 +286,7 @@ namespace Barotrauma /// If the rectT height is set 0, the height is calculated from the text. /// public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, - bool playerInput = false) + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { if (color.HasValue) @@ -307,7 +305,6 @@ namespace Barotrauma this.textAlignment = textAlignment; this.Wrap = wrap; this.Text = text ?? ""; - this.playerInput = playerInput; if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { CalculateHeightFromText(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 2bcb6bbb7..866214dd6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -251,6 +251,8 @@ namespace Barotrauma public bool Readonly { get; set; } + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -261,7 +263,7 @@ namespace Barotrauma this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); - textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap, playerInput: true); + textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap); GUIStyle.Apply(textBlock, "", this); if (font != null) { textBlock.Font = font; } CaretEnabled = true; @@ -350,7 +352,7 @@ namespace Barotrauma caretPosDirty = false; } - public void Select(int forcedCaretIndex = -1) + public void Select(int forcedCaretIndex = -1, bool ignoreSelectSound = false) { skipUpdate = true; if (memento.Current == null) @@ -360,9 +362,14 @@ namespace Barotrauma CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; CalculateCaretPos(); ClearSelection(); + bool wasSelected = selected; selected = true; GUI.KeyboardDispatcher.Subscriber = this; OnSelected?.Invoke(this, Keys.None); + if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Deselect() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 05e59d5fc..47ce9cab1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -1,15 +1,13 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; namespace Barotrauma { public class GUITickBox : GUIComponent { - private GUILayoutGroup layoutGroup; - private GUIFrame box; - private GUITextBlock text; + private readonly GUILayoutGroup layoutGroup; + private readonly GUIFrame box; + private readonly GUITextBlock text; public delegate bool OnSelectedHandler(GUITickBox obj); public OnSelectedHandler OnSelected; @@ -129,6 +127,12 @@ namespace Barotrauma set { text.Text = value; } } + public float ContentWidth { get; private set; } + + public GUISoundType SoundType { private get; set; } = GUISoundType.TickBox; + + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITickBox(RectTransform rectT, LocalizedString label, GUIFont font = null, string style = "") : base(null, rectT) { CanBeFocused = true; @@ -180,6 +184,7 @@ namespace Barotrauma box.RectTransform.MinSize = new Point(Rect.Height); box.RectTransform.Resize(box.RectTransform.MinSize); text.SetTextPos(); + ContentWidth = box.Rect.Width + text.Padding.X + text.TextSize.X + text.Padding.Z; } protected override void Update(float deltaTime) @@ -209,6 +214,10 @@ namespace Barotrauma { Selected = true; } + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(SoundType); + } } } else if (isSelected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ad3b9c4d8..1c2e869b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -14,7 +15,7 @@ namespace Barotrauma get { return inventoryTopY; } set { - if (value == inventoryTopY) return; + if (value == inventoryTopY) { return; } inventoryTopY = value; CreateAreas(); } @@ -90,8 +91,6 @@ namespace Barotrauma if (GameMain.Instance != null) { GameMain.Instance.ResolutionChanged += CreateAreas; - #warning TODO: reimplement - //GameSettings.CurrentConfig.OnHUDScaleChanged += CreateAreas; CreateAreas(); CharacterInfo.Init(); } @@ -122,7 +121,17 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.58f); + int healthBarWidth = BottomRightInfoArea.Width; + + var healthBarChildStyles = GUIStyle.GetComponentStyle("CharacterHealthBar")?.ChildStyles; + if (healthBarChildStyles!= null && healthBarChildStyles.TryGetValue("GUIFrame".ToIdentifier(), out var style)) + { + if (style.Sprites.TryGetValue(GUIComponent.ComponentState.None, out var uiSprites) && uiSprites.FirstOrDefault() is { } uiSprite) + { + // The default health bar uses a sliced sprite so let's make sure the health bar area is calculated accordingly + healthBarWidth += (int)(uiSprite.NonSliceSize.X * Math.Min(GUI.Scale, 1f)); + } + } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 211f69381..54373bcff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -569,6 +569,7 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("medicalclinic.heal")) { + ClickSound = GUISoundType.ConfirmTransaction, Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetBalance() >= medicalClinic.GetTotalCost(), OnClicked = (button, _) => { @@ -595,6 +596,7 @@ namespace Barotrauma GUIButton clearButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -657,6 +659,7 @@ namespace Barotrauma { CanBeFocused = false }; + GUILayoutGroup parentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, backgroundFrame.RectTransform), isHorizontal: true) { Stretch = true }; if (!(affliction.Prefab is { } prefab)) { return; } @@ -676,13 +679,14 @@ namespace Barotrauma GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUIStyle.SubHeadingFont); - GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -746,12 +750,12 @@ namespace Barotrauma ClosePopup(); - GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.45f), container.RectTransform) + GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.5f), container.RectTransform) { ScreenSpaceOffset = location.ToPoint() }); - GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; if (mainFrame.Rect.Bottom > GameMain.GraphicsHeight) { @@ -765,6 +769,7 @@ namespace Barotrauma GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) { + ClickSound = GUISoundType.Cart, Font = GUIStyle.SubHeadingFont, Visible = false }; @@ -819,7 +824,9 @@ namespace Barotrauma if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); - GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), backgroundFrame.RectTransform, Anchor.Center)) + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), backgroundFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.05f }; @@ -862,15 +869,32 @@ namespace Barotrauma GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)); - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUIStyle.Font, GUI.IntScale(64)), wrap: true) + GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)) + { + RelativeSpacing = 0.05f + }; + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true) { ToolTip = prefab.Description }; + bool truncated = false; + while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n')) + { + var split = descriptionBlock.WrappedText.Value.Split('\n'); + descriptionBlock.Text = string.Join('\n', split.Take(split.Length - 1)); + truncated = true; + } + if (truncated) + { + descriptionBlock.Text += "..."; + } - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); - GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); + GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton") + { + ClickSound = GUISoundType.Cart + }; ImmutableArray elementsToDisable = ImmutableArray.Create(prefabBlock, backgroundFrame, icon, vitalityBlock, severityBlock, buyButton, descriptionBlock, priceBlock); @@ -923,6 +947,7 @@ namespace Barotrauma }); } + #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 public static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) { if (string.IsNullOrWhiteSpace(text)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index e51c586c9..f27eed481 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -46,9 +46,7 @@ namespace Barotrauma private int buyTotal, sellTotal, sellFromSubTotal; private GUITextBlock storeNameBlock; - private GUITextBlock merchantBalanceBlock; - private GUITextBlock currentSellValueBlock, newSellValueBlock; - private GUIImage sellValueChangeArrow; + private GUITextBlock reputationEffectBlock; private GUIDropDown sortingDropDown; private GUITextBox searchBox; private GUILayoutGroup categoryButtonContainer; @@ -376,41 +374,29 @@ namespace Barotrauma AutoScaleVertical = true, ForceUpperCase = ForceUpperCase.Yes }; - merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - "", font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), "", + color: Color.White, font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => - { - merchantBalanceBlock.TextColor = ActiveStore?.BalanceColor ?? Color.Red; - return GetMerchantBalanceText(); - } + TextGetter = () => GetMerchantBalanceText() }; // Item sell value ------------------------------------------------ - var sellValueContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) + var reputationEffectContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) { CanBeFocused = true, - RelativeSpacing = 0.005f + RelativeSpacing = 0.005f, + ToolTip = TextManager.Get("campaignstore.reputationtooltip") }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), - TextManager.Get("campaignstore.sellvalue"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), + TextManager.Get("reputationmodifier"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, - ForceUpperCase = ForceUpperCase.Yes + ForceUpperCase = ForceUpperCase.Yes, }; - - var valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - CanBeFocused = false, - RelativeSpacing = 0.02f - }; - float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; - Point blockMaxSize = new Point((int)(GameSettings.CurrentConfig.Graphics.TextScale * 60), valueChangeGroup.Rect.Height); - currentSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUIStyle.SubHeadingFont) + reputationEffectBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -419,64 +405,27 @@ namespace Barotrauma { if (CurrentLocation != null) { - int balanceAfterTransaction = activeTab switch + Color textColor = GUIStyle.ColorReputationNeutral; + string sign = ""; + int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + if (reputationModifier > 0) { - StoreTab.Buy => ActiveStore.Balance + buyTotal, - StoreTab.Sell => ActiveStore.Balance - sellTotal, - StoreTab.SellSub => ActiveStore.Balance - sellFromSubTotal, - _ => throw new NotImplementedException(), - }; - if (balanceAfterTransaction != ActiveStore.Balance) - { - var newStatus = CurrentLocation.GetStoreBalanceStatus(balanceAfterTransaction); - if (ActiveStore.ActiveBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) - { - string tooltipTag = newStatus.SellPriceModifier > ActiveStore.ActiveBalanceStatus.SellPriceModifier ? - "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; - sellValueContainer.ToolTip = TextManager.Get(tooltipTag); - currentSellValueBlock.TextColor = newStatus.Color; - sellValueChangeArrow.Color = newStatus.Color; - sellValueChangeArrow.Visible = true; - newSellValueBlock.TextColor = newStatus.Color; - newSellValueBlock.Text = $"{(newStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - } + textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; + sign = "+"; } - sellValueContainer.ToolTip = TextManager.Get("campaignstore.sellvaluetooltip"); - currentSellValueBlock.TextColor = ActiveStore.BalanceColor; - sellValueChangeArrow.Visible = false; - newSellValueBlock.Text = null; - return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + else if (reputationModifier < 0) + { + textColor = IsBuying ? GUIStyle.ColorReputationHigh : GUIStyle.ColorReputationLow; + } + reputationEffectBlock.TextColor = textColor; + return $"{sign}{reputationModifier}%"; } else { - sellValueContainer.ToolTip = null; - sellValueChangeArrow.Visible = false; - newSellValueBlock.Text = null; - return null; + return ""; } } }; - Vector4 newPadding = currentSellValueBlock.Padding; - newPadding.Z = 0; - currentSellValueBlock.Padding = newPadding; - float relativeHeight = 0.45f; - float relativeWidth = (relativeHeight * valueChangeGroup.Rect.Height) / valueChangeGroup.Rect.Width; - sellValueChangeArrow = new GUIImage(new RectTransform(new Vector2(relativeWidth, relativeHeight), valueChangeGroup.RectTransform), "StoreArrow", scaleToFit: true) - { - CanBeFocused = false, - Visible = false - }; - newSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUIStyle.SubHeadingFont) - { - AutoScaleVertical = true, - CanBeFocused = false, - TextScale = 1.1f - }; - newPadding = newSellValueBlock.Padding; - newPadding.X = 0; - newSellValueBlock.Padding = newPadding; // Store mode buttons ------------------------------------------------ var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f / 14.0f), storeContent.RectTransform), style: null); @@ -707,7 +656,7 @@ namespace Barotrauma SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = HasActiveTabPermissions(), ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => @@ -1597,7 +1546,7 @@ namespace Barotrauma { RelativeSpacing = 0.02f }; - amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), GUINumberInput.NumberType.Int) + amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = GetMaxAvailable(pi.ItemPrefab, containingTab), @@ -1618,8 +1567,6 @@ namespace Barotrauma } AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity); }; - amountInput.PlusButton.ClickSound = GUISoundType.IncreaseQuantity; - amountInput.MinusButton.ClickSound = GUISoundType.DecreaseQuantity; frame.HoverColor = frame.SelectedColor = Color.Transparent; } @@ -1673,7 +1620,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { - ClickSound = GUISoundType.IncreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable && pi.Quantity > 0, ForceUpperCase = ForceUpperCase.Yes, UserData = "addbutton", @@ -1684,7 +1631,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton") { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable, ForceUpperCase = ForceUpperCase.Yes, UserData = "removebutton", @@ -2127,11 +2074,13 @@ namespace Barotrauma { if (IsBuying) { + confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); confirmButton.OnClicked = (b, o) => BuyItems(); } else { + confirmButton.ClickSound = GUISoundType.Select; confirmButton.Text = TextManager.Get("CampaignStoreTab.Sell"); confirmButton.OnClicked = (b, o) => { @@ -2139,6 +2088,7 @@ namespace Barotrauma TextManager.Get("FireWarningHeader"), TextManager.Get("CampaignStore.SellWarningText"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDialog.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems(); confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; @@ -2258,7 +2208,7 @@ namespace Barotrauma } updateStopwatch.Stop(); - GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession:Store", updateStopwatch.ElapsedTicks); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 35ec743ca..28de3502a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -29,6 +29,8 @@ namespace Barotrauma private GUITextBlock descriptionTextBlock; private int selectionIndicatorThickness; private GUIImage listBackground; + private GUITickBox transferItemsTickBox; + private GUITextBlock itemTransferReminderBlock; private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; @@ -61,6 +63,23 @@ namespace Barotrauma public GUIButton previewButton; } + private bool TransferItemsOnSwitch + { + get + { + return transferItemsOnSwitch; + } + set + { + transferItemsOnSwitch = value; + if (transferItemsTickBox != null) + { + transferItemsTickBox.Selected = value; + } + } + } + private bool transferItemsOnSwitch = true; + public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) { if (GameMain.GameSession.Campaign == null) { return; } @@ -149,11 +168,12 @@ namespace Barotrauma GUIListBox descriptionFrame = new GUIListBox(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform), style: null) { Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding / 2f) }; descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUIStyle.Font, wrap: true) { CanBeFocused = false }; - GUILayoutGroup buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), content.RectTransform), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + GUILayoutGroup bottomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), content.RectTransform, Anchor.CenterRight), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + float transferInfoFrameWidth = 1.0f; if (closeAction != null) { - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") { OnClicked = (button, userData) => { @@ -161,11 +181,33 @@ namespace Barotrauma return true; } }; + transferInfoFrameWidth -= closeButton.RectTransform.RelativeSize.X; } - if (purchaseService) confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + if (purchaseService) + { + confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); + transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; + } + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); + transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; + GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) + { + CanBeFocused = false + }; + transferItemsTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 1.0f), transferInfoFrame.RectTransform, Anchor.CenterRight), TextManager.Get("transferitems"), font: GUIStyle.SubHeadingFont) + { + Selected = TransferItemsOnSwitch, + Visible = false, + OnSelected = (tb) => transferItemsOnSwitch = tb.Selected + }; + transferItemsTickBox.RectTransform.Resize(new Point(Math.Min((int)transferItemsTickBox.ContentWidth, transferInfoFrame.Rect.Width), transferItemsTickBox.Rect.Height)); + itemTransferReminderBlock = new GUITextBlock(new RectTransform(Vector2.One, transferInfoFrame.RectTransform, Anchor.CenterRight), null) + { + TextAlignment = Alignment.CenterRight, + Visible = false + }; pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); pageIndicator = GUIStyle.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); @@ -272,7 +314,7 @@ namespace Barotrauma } } - public void RefreshSubmarineDisplay(bool updateSubs) + public void RefreshSubmarineDisplay(bool updateSubs, bool setTransferOptionToTrue = false) { if (!initialized) { @@ -286,6 +328,10 @@ namespace Barotrauma { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (setTransferOptionToTrue) + { + TransferItemsOnSwitch = true; + } if (updateSubs) { UpdateSubmarines(); @@ -401,6 +447,10 @@ namespace Barotrauma { SelectSubmarine(null, Rectangle.Empty); } + else + { + UpdateItemTransferInfoFrame(); + } } private void UpdateSubmarines() @@ -553,6 +603,40 @@ namespace Barotrauma selectedSubmarineIndicator.RectTransform.NonScaledSize = Point.Zero; SetConfirmButtonState(false); } + + UpdateItemTransferInfoFrame(); + } + + private void UpdateItemTransferInfoFrame() + { + if (selectedSubmarine != null) + { + var pendingSub = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (Submarine.MainSub?.Info?.Name == selectedSubmarine.Name && pendingSub == null) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } + else if (pendingSub?.Name == selectedSubmarine.Name) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Text = GameMain.GameSession.Campaign.TransferItemsOnSubSwitch ? + TextManager.Get("itemtransferenabledreminder") : + TextManager.Get("itemtransferdisabledreminder"); + itemTransferReminderBlock.Visible = true; + } + else + { + transferItemsTickBox.Selected = TransferItemsOnSwitch; + transferItemsTickBox.Visible = true; + itemTransferReminderBlock.Visible = false; + } + } + else + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } } private void SetConfirmButtonState(bool state) @@ -614,24 +698,27 @@ namespace Barotrauma ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), ("[amount]", deliveryFee.ToString()), ("[currencyname]", currencyName)), messageBoxOptions); + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; } else { - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", + var text = TextManager.GetWithVariables("switchsubmarinetext", ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)), messageBoxOptions); + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.SwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.SwitchSub); } return true; }; @@ -653,23 +740,25 @@ namespace Barotrauma if (!purchaseOnly) { - msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", + var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), ("[currencyname]", currencyName), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), text, messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseAndSwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.PurchaseAndSwitchSub); } return true; }; @@ -690,14 +779,20 @@ namespace Barotrauma } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, false, Networking.VoteType.PurchaseSub); } return true; }; } + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = msgBox.Close; - } + } + + private LocalizedString GetItemTransferText() + { + return "\n\n" + TextManager.Get(TransferItemsOnSwitch ? "itemswillbetransferred" : "itemswontbetransferred"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8530291a5..ecdb03592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -54,47 +54,65 @@ namespace Barotrauma private ushort currentPing; private readonly Character character; - private readonly bool hasCharacter; + private readonly bool wasCharacterAlive; private readonly GUITextBlock textBlock; private readonly GUIFrame frame; private readonly GUIImage permissionIcon; - public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock, GUIImage permissionIcon) + public LinkedGUI(Client client, GUIFrame frame, GUITextBlock textBlock, GUIImage permissionIcon) { this.Client = client; this.textBlock = textBlock; this.frame = frame; - this.hasCharacter = hasCharacter; this.permissionIcon = permissionIcon; + character = client?.Character; + wasCharacterAlive = client?.Character != null && !client.Character.IsDead; } - public LinkedGUI(Character character, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + public LinkedGUI(Character character, GUIFrame frame, GUITextBlock textBlock) { this.character = character; this.textBlock = textBlock; this.frame = frame; - this.hasCharacter = hasCharacter; + wasCharacterAlive = character != null && !character.IsDead; } public bool HasMultiplayerCharacterChanged() { if (Client == null) { return false; } - bool characterState = Client.Character != null; - if (characterState && Client.Character.IsDead) characterState = false; - return hasCharacter != characterState; + + if (GameSettings.CurrentConfig.VerboseLogging) + { + if (Client.Character != character) + { + DebugConsole.Log($"Refreshing tab menu crew list (client \"{Client.Name}\"'s character changed from \"{character?.Name ?? "null"}\" to \"{Client.Character?.Name ?? "null"}\")"); + } + } + return Client.Character != character; } - public bool HasMultiplayerCharacterDied() - { - if (Client == null || !hasCharacter || Client.Character == null) { return false; } - return Client.Character.IsDead; - } - - public bool HasAICharacterDied() + public bool HasCharacterDied() { if (character == null) { return false; } - return character.IsDead; + bool isAlive = !(character?.IsDead ?? true); + if (GameSettings.CurrentConfig.VerboseLogging) + { + if (wasCharacterAlive && !isAlive) + { + DebugConsole.Log(Client == null ? + $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" died)" : + $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" died)"); + } + else if (!wasCharacterAlive && isAlive) + { + DebugConsole.Log(Client == null ? + + $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" came back to life)" : + $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" came back to life)"); + } + } + return isAlive != wasCharacterAlive; } public void TryPingRefresh() @@ -207,7 +225,7 @@ namespace Barotrauma { linkedGUIList[i].TryPingRefresh(); linkedGUIList[i].TryPermissionIconRefresh(GetPermissionIcon(linkedGUIList[i].Client)); - if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasMultiplayerCharacterDied() || linkedGUIList[i].HasAICharacterDied()) + if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasCharacterDied()) { RemoveCurrentElements(); CreateMultiPlayerList(true); @@ -219,10 +237,11 @@ namespace Barotrauma { for (int i = 0; i < linkedGUIList.Count; i++) { - if (linkedGUIList[i].HasAICharacterDied()) + if (linkedGUIList[i].HasCharacterDied()) { RemoveCurrentElements(); CreateSinglePlayerList(true); + return; } } } @@ -297,6 +316,10 @@ namespace Barotrauma var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right); + if (GameMain.IsMultiplayer) + { + balanceText.ToolTip = TextManager.Get("bankdescription"); + } GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform) { AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) @@ -337,7 +360,7 @@ namespace Barotrauma var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); talentsButton.OnAddedToGUIUpdateList += (component) => { - talentsButton.Enabled = Character.Controlled?.Info != null; + talentsButton.Enabled = Character.Controlled?.Info != null || GameMain.Client?.CharacterInfo != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { SelectInfoFrameTab(InfoFrameTab.Crew); @@ -430,7 +453,8 @@ namespace Barotrauma GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform)) { Padding = new Vector4(2, 5, 0, 0), - AutoHideScrollBar = false + AutoHideScrollBar = false, + PlaySoundOnSelect = true }; crewList.UpdateDimensions(); @@ -560,7 +584,7 @@ namespace Barotrauma GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, textBlock: null)); + linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null)); } private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) @@ -657,7 +681,7 @@ namespace Barotrauma if (client != null) { CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); - linkedGUIList.Add(new LinkedGUI(client, frame, true, + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); } @@ -668,12 +692,12 @@ namespace Barotrauma if (character is AICharacter) { - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, + linkedGUIList.Add(new LinkedGUI(character, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes })); } else { - linkedGUIList.Add(new LinkedGUI(client: null, frame, true, textBlock: null, permissionIcon: null)); + linkedGUIList.Add(new LinkedGUI(client: null, frame, textBlock: null, permissionIcon: null)); new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect)) { @@ -718,7 +742,7 @@ namespace Barotrauma }; CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); - linkedGUIList.Add(new LinkedGUI(client, frame, false, + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); @@ -775,19 +799,27 @@ namespace Barotrauma Stretch = true }; + new GUIFrame(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("walletdescription") + }; + if (character.IsBot) { return; } Sprite walletSprite = GUIStyle.CrewWalletIconSmall.Value.Sprite; - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true); + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true) { CanBeFocused = false }; GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.Font) { AutoScaleHorizontal = true, - Padding = Vector4.Zero + Padding = Vector4.Zero, + CanBeFocused = false }; GUIImage largeIcon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), walletSprite, scaleToFit: true) { + CanBeFocused = false, IgnoreLayoutGroups = true, Visible = false }; @@ -897,8 +929,8 @@ namespace Barotrauma } else { - Vector2 stringOffset = GUIStyle.GlobalFont.MeasureString(inLobbyString) / 2f; - GUIStyle.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + Vector2 stringOffset = GUIStyle.Font.MeasureString(inLobbyString) / 2f; + GUIStyle.Font.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); } } @@ -971,16 +1003,25 @@ namespace Barotrauma float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X; GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont); + GUIFrame walletTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, headerLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("walletdescription") + }; GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform)); GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true); GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight); + GUIFrame salaryTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, middleLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("crewwallet.salary.tooltip") + }; GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f) { - ToolTip = TextManager.Get("crewwallet.salary.tooltip"), Range = new Vector2(0, 1), BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, @@ -1024,7 +1065,7 @@ namespace Barotrauma GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(1f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow"); GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); - GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) + GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, hidePlusMinusButtons: true) { MinValueInt = 0 }; @@ -1050,149 +1091,195 @@ namespace Barotrauma return true; } }; + + Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier(); + ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen); ToggleCenterButton(centerButton, isSending); - - if (!(Character.Controlled is { } myCharacter)) - { - salarySlider.Enabled = false; - transferAmountInput.Enabled = false; - centerButton.Enabled = false; - confirmButton.Enabled = false; - return; - } - - bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets(); - salarySlider.Enabled = hasMoneyPermissions; Wallet otherWallet; + GameMain.Client?.OnPermissionChanged.RegisterOverwriteExisting(eventIdentifier, e => UpdateWalletInterface(registerEvents: false)); + UpdateWalletInterface(registerEvents: true); - switch (hasMoneyPermissions) + void UpdateWalletInterface(bool registerEvents) { - case true: - rightName.Text = TextManager.Get("crewwallet.bank"); - otherWallet = campaign.Bank; - break; - case false when character == myCharacter: - rightName.Text = TextManager.Get("crewwallet.bank"); - otherWallet = campaign.Bank; - isSending = true; - ToggleCenterButton(centerButton, isSending); - break; - default: - rightName.Text = myCharacter.Name; - otherWallet = campaign.PersonalWallet; - break; - } - - MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); - updateButtonText(); - if (!hasMoneyPermissions) - { - if (character != Character.Controlled) + if (!(Character.Controlled is { } myCharacter)) { - centerButton.Enabled = centerButton.CanBeFocused = false; + salarySlider.Enabled = false; + transferAmountInput.Enabled = false; + centerButton.Enabled = false; + confirmButton.Enabled = false; + return; } - salarySlider.Enabled = salarySlider.CanBeFocused = false; - } - leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets(); + salarySlider.Enabled = hasMoneyPermissions; - UpdateAllInputs(); + switch (hasMoneyPermissions) + { + case true: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + break; + case false when character == myCharacter: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + isSending = true; + ToggleCenterButton(centerButton, isSending); + break; + default: + rightName.Text = myCharacter.Name; + otherWallet = campaign.PersonalWallet; + break; + } + + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + + UpdatedConfirmButtonText(); + + if (!hasMoneyPermissions) + { + if (character != Character.Controlled) + { + centerButton.Enabled = centerButton.CanBeFocused = false; + } + + salarySlider.Enabled = salarySlider.CanBeFocused = false; + } + + leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); - centerButton.OnClicked = (btn, o) => - { - isSending = !isSending; - updateButtonText(); - ToggleCenterButton(btn, isSending); UpdateAllInputs(); - return true; - }; - void updateButtonText() - { - confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney"); - } + if (!registerEvents) { return; } - transferAmountInput.OnValueChanged = input => - { - UpdateInputs(); - }; - - transferAmountInput.OnValueEntered = input => - { - UpdateAllInputs(); - }; - - Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier(); - campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => - { - if (e.Wallet == targetWallet) + centerButton.OnClicked = (btn, o) => { - moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); - salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + isSending = !isSending; + UpdatedConfirmButtonText(); + ToggleCenterButton(btn, isSending); + UpdateAllInputs(); + return true; + }; + + transferAmountInput.OnValueChanged = input => + { + UpdateInputs(); + }; + + transferAmountInput.OnValueEntered = input => + { + UpdateAllInputs(); + }; + + resetButton.OnClicked = (button, o) => + { + transferAmountInput.IntValue = 0; + UpdateAllInputs(); + return true; + }; + + confirmButton.OnClicked = (button, o) => + { + int amount = transferAmountInput.IntValue; + if (amount == 0) { return false; } + + Option target1 = Option.Some(character), + target2 = otherWallet == campaign.Bank ? Option.None() : Option.Some(myCharacter); + if (isSending) { (target1, target2) = (target2, target1); } + + SendTransaction(target1, target2, amount); + isTransferMenuOpen = false; + ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen); + return true; + }; + + campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => + { + if (e.Wallet == targetWallet) + { + moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); + salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + } + + UpdateAllInputs(); + }); + + registeredEvents.Add(eventIdentifier); + + void UpdatedConfirmButtonText() + { + confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney"); } - UpdateAllInputs(); - }); - registeredEvents.Add(eventIdentifier); - resetButton.OnClicked = (button, o) => - { - transferAmountInput.IntValue = 0; - UpdateAllInputs(); - return true; - }; - - confirmButton.OnClicked = (button, o) => - { - int amount = transferAmountInput.IntValue; - if (amount == 0) { return false; } - - Option target1 = Option.Some(character), - target2 = otherWallet == campaign.Bank ? Option.None() : Option.Some(myCharacter); - if (isSending) { (target1, target2) = (target2, target1); } - - SendTransaction(target1, target2, amount); - isTransferMenuOpen = false; - ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen); - return true; - }; - - void UpdateAllInputs() - { - UpdateInputs(); - UpdateMaxInput(); - } - - void UpdateInputs() - { - confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; - if (transferAmountInput.IntValue == 0) + void UpdateAllInputs() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); - rightBalance.TextColor = GUIStyle.TextColorNormal; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); - leftBalance.TextColor = GUIStyle.TextColorNormal; + UpdateInputs(); + UpdateMaxInput(); } - else if (isSending) + + void UpdateInputs() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); - rightBalance.TextColor = GUIStyle.Blue; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); - leftBalance.TextColor = GUIStyle.Red; + confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; + if (transferAmountInput.IntValue == 0) + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + rightBalance.TextColor = GUIStyle.TextColorNormal; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); + leftBalance.TextColor = GUIStyle.TextColorNormal; + } + else if (isSending) + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Blue; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Red; + } + else + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Red; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Blue; + } } - else + + void UpdateMaxInput() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); - rightBalance.TextColor = GUIStyle.Red; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); - leftBalance.TextColor = GUIStyle.Blue; + int maxValue = isSending ? targetWallet.Balance : otherWallet.Balance; + transferAmountInput.MaxValueInt = maxValue; + + transferAmountInput.Enabled = true; + transferAmountInput.ToolTip = string.Empty; + + if (!hasMoneyPermissions && GameMain.Client?.ServerSettings is { } serverSettings) + { + transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumMoneyTransferRequest); + if (serverSettings.MaximumMoneyTransferRequest <= 0) + { + transferAmountInput.Enabled = false; + transferAmountInput.ToolTip = TextManager.Get("wallettransferrequestdisabled"); + } + } } } - void UpdateMaxInput() + void SetRewardText(int value, GUITextBlock block) { - transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; + var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option.None()); + LocalizedString tooltip = string.Empty; + block.TextColor = GUIStyle.TextColorNormal; + + if (sum > 100) + { + tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}")); + block.TextColor = GUIStyle.Orange; + } + + LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}"); + + block.Text = text; + block.ToolTip = RichString.Rich(tooltip); } static void ToggleTransferMenuIcon(GUIButton btn, bool open) @@ -1235,24 +1322,6 @@ namespace Barotrauma transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - - void SetRewardText(int value, GUITextBlock block) - { - var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option.None()); - LocalizedString tooltip = string.Empty; - block.TextColor = GUIStyle.TextColorNormal; - - if (sum > 100) - { - tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}")); - block.TextColor = GUIStyle.Orange; - } - - LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}"); - - block.Text = text; - block.ToolTip = RichString.Rich(tooltip); - } } private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) @@ -1490,10 +1559,10 @@ namespace Barotrauma RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString); - Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString.SanitizedValue); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString.SanitizedValue); + Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString.SanitizedValue); + Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString.SanitizedValue); float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; bool displayDifficulty = mission.Difficulty.HasValue; @@ -1740,9 +1809,6 @@ namespace Barotrauma talentButtons.Clear(); talentCornerIcons.Clear(); - Character controlledCharacter = Character.Controlled; - if (controlledCharacter == null) { return; } - GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = GUI.IntScale(15); GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); @@ -1762,13 +1828,20 @@ namespace Barotrauma GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); } + /*Character controlledCharacter = Character.Controlled; + if (controlledCharacter == null) { return; } + if (controlledCharacter.Info is null) { DebugConsole.ThrowError("No character info found for talent UI"); return; - } + }*/ - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); + Character controlledCharacter = Character.Controlled; + CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; + if (info == null) { return; } + + Job job = info.Job; GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameMain.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -1776,9 +1849,7 @@ namespace Barotrauma }; GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); - - CharacterInfo info = controlledCharacter.Info; - Job job = info.Job; + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), talentInfoLayoutGroup.RectTransform), onDraw: (batch, component) => { @@ -1787,25 +1858,30 @@ namespace Barotrauma }); GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; - + Vector2 nameSize = GUIStyle.SubHeadingFont.MeasureString(info.Name); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont) { TextColor = job.Prefab.UIColor }; + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); - Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; - jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + if (!info.OmitJobInMenus) + { + nameBlock.TextColor = job.Prefab.UIColor; + Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; + jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + } LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - GUIFrame endocrineFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); + GUIFrame talentsOutsideTreeFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); if (!(GameMain.NetworkMember is null)) { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), endocrineFrame.RectTransform, Anchor.TopLeft), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), talentsOutsideTreeFrame.RectTransform, Anchor.TopLeft), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) { IgnoreLayoutGroups = true }; @@ -1844,6 +1920,7 @@ namespace Barotrauma { OnClicked = (button, o) => { + GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); characterSettingsFrame!.Visible = false; talentFrameMain.Visible = true; return true; @@ -1852,13 +1929,14 @@ namespace Barotrauma } } - IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); + IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); - if (endocrineTalents.Count() > 0) + if (talentsOutsideTree.Count() > 0) { - GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), endocrineFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") + //TODO: replace with something more generic + GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), talentsOutsideTreeFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") { - ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", endocrineTalents.Select(e => e.DisplayName))}" + ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", talentsOutsideTree.Select(e => e.DisplayName))}" }; } @@ -1870,49 +1948,55 @@ namespace Barotrauma skillBlock.RectTransform.NonScaledSize = skillSize.Pad(skillBlock.Padding).ToPoint(); skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); - CreateTalentSkillList(controlledCharacter, skillListBox); + CreateTalentSkillList(controlledCharacter, info, skillListBox); - if (!TalentTree.JobTalentTrees.TryGet(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); - - GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); - - List subTreeNames = new List(); - foreach (var subTree in talentTree.TalentSubTrees) + if (controlledCharacter != null) { - GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); - GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); + if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - int elementPadding = GUI.IntScale(8); - Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; - GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); + new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); - for (int i = 0; i < 4; i++) + GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + + selectedTalents = info.GetUnlockedTalentsInTree().ToList(); + + List subTreeNames = new List(); + foreach (var subTree in talentTree.TalentSubTrees) { - GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); - Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + int elementPadding = GUI.IntScale(8); + Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; + GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); - GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground"); - GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; - - GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + for (int i = 0; i < 4; i++) { - CanBeFocused = false - }; + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - Point iconSize = cornerIcon.RectTransform.NonScaledSize; - cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + + GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground") + { + Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] + }; + GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; + + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + { + CanBeFocused = false, + Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] + }; + + Point iconSize = cornerIcon.RectTransform.NonScaledSize; + cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + + if (subTree.TalentOptionStages.Count <= i) { continue; } - if (subTree.TalentOptionStages.Count > i) - { TalentOption talentOption = subTree.TalentOptionStages[i]; - GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); - GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; foreach (TalentPrefab talent in talentOption.Talents.OrderBy(t => t.Identifier)) @@ -1929,6 +2013,7 @@ namespace Barotrauma ToolTip = RichString.Rich(talent.DisplayName + "\n\n" + talent.Description), UserData = talent.Identifier, PressedColor = pressedColor, + Enabled = controlledCharacter != null, OnClicked = (button, userData) => { // deselect other buttons in tier by removing their selected talents from pool @@ -1961,7 +2046,7 @@ namespace Barotrauma }, }; - talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = Color.Transparent; + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; GUIComponent iconImage; if (talent.Icon is null) @@ -1971,6 +2056,7 @@ namespace Barotrauma OutlineColor = GUIStyle.Red, TextColor = GUIStyle.Red, PressedColor = unselectableColor, + DisabledColor = unselectableColor, CanBeFocused = false, }; } @@ -1979,63 +2065,63 @@ namespace Barotrauma iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) { PressedColor = unselectableColor, + DisabledColor = unselectableColor * 0.5f, CanBeFocused = false, }; } - + iconImage.Enabled = talentButton.Enabled; talentButtons.Add((talentButton, iconImage)); } - - talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); + talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); } } - } - GUITextBlock.AutoScaleAndNormalize(subTreeNames); + GUITextBlock.AutoScaleAndNormalize(subTreeNames); - GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; + GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); - GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); - experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), - barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) - { - IsHorizontal = true, - }; + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) + { + IsHorizontal = true, + }; - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) - { - Shadow = true, - ToolTip = TextManager.Get("experiencetooltip") - }; + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) + { + Shadow = true, + ToolTip = TextManager.Get("experiencetooltip") + }; - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; - talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") - { - OnClicked = ResetTalentSelection - }; - talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") - { - OnClicked = ApplyTalentSelection, - }; - GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + { + OnClicked = ApplyTalentSelection, + }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + } UpdateTalentInfo(); } - private void CreateTalentSkillList(Character character, GUIListBox parent) + private void CreateTalentSkillList(Character character, CharacterInfo info, GUIListBox parent) { parent.Content.ClearChildren(); List skillNames = new List(); - foreach (Skill skill in character.Info.Job.GetSkills()) + foreach (Skill skill in info.Job.GetSkills()) { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value))); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; - float modifiedSkillLevel = character.GetSkillLevel(skill.Identifier); + float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); @@ -2129,7 +2215,7 @@ namespace Barotrauma talentButton.icon.HoverColor = hoverColor; } - CreateTalentSkillList(controlledCharacter, skillListBox); + CreateTalentSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); } private void ApplyTalents(Character controlledCharacter) @@ -2157,6 +2243,7 @@ namespace Barotrauma private bool ResetTalentSelection(GUIButton guiButton, object userData) { Character controlledCharacter = Character.Controlled; + if (controlledCharacter?.Info == null) { return false; } selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); UpdateTalentInfo(); return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 54c5ee076..fc8dfd1f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -27,6 +27,11 @@ namespace Barotrauma set; } + /// + /// The size of fixed area around the slice area + /// + public Point NonSliceSize { get; set; } + public bool MaintainAspectRatio { get; @@ -72,6 +77,7 @@ namespace Barotrauma maxBorderScale = element.GetAttributeFloat("minborderscale", 10.0f); Rectangle slice = new Rectangle((int)sliceVec.X, (int)sliceVec.Y, (int)(sliceVec.Z - sliceVec.X), (int)(sliceVec.W - sliceVec.Y)); + NonSliceSize = new Point(Sprite.SourceRect.Width - slice.Width, Sprite.SourceRect.Height - slice.Height); Slices = new Rectangle[9]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f694cb4d5..6833b74d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -462,7 +462,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -497,7 +497,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -539,7 +539,7 @@ namespace Barotrauma GameMain.Client?.SendCampaignState(); } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -589,7 +589,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); @@ -622,7 +622,8 @@ namespace Barotrauma PadBottom = true, SelectTop = true, ClampScrollToElements = true, - Spacing = 8 + Spacing = 8, + PlaySoundOnSelect = true }; Dictionary> upgrades = new Dictionary>(); @@ -1123,7 +1124,10 @@ namespace Barotrauma { priceText.Text = string.Empty; } - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) { Enabled = false }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) + { + Enabled = false + }; if (upgradePrefab != null) { var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); @@ -1212,7 +1216,7 @@ namespace Barotrauma Campaign.UpgradeManager.PurchaseUpgrade(prefab, category); GameMain.Client?.SendCampaignState(); return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); return true; }; @@ -1400,7 +1404,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { - ScrollToCategory(data => data.Category.IsWallUpgrade); + ScrollToCategory(data => data.Category.IsWallUpgrade, GUIListBox.PlaySelectSound.Yes); } } } @@ -1682,7 +1686,7 @@ namespace Barotrauma } } - private void ScrollToCategory(Predicate predicate) + private void ScrollToCategory(Predicate predicate, GUIListBox.PlaySelectSound playSelectSound = GUIListBox.PlaySelectSound.No) { if (currentStoreLayout == null) { return; } @@ -1690,7 +1694,7 @@ namespace Barotrauma { if (child.UserData is CategoryData data && predicate(data)) { - currentStoreLayout.ScrollToElement(child); + currentStoreLayout.ScrollToElement(child, playSelectSound); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index e54913559..7d83f6e99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -26,7 +26,7 @@ namespace Barotrauma private Color SubmarineColor => GUIStyle.Orange; private Point createdForResolution; - public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) + public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime) { if (starter == null || info == null) { return null; } @@ -38,7 +38,7 @@ namespace Barotrauma getMaxVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountMax(type) ?? 0, }; subVoting.onVoteEnd = () => subVoting.SendSubmarineVoteEndMessage(info, type); - subVoting.SetSubmarineVotingText(starter, info, type); + subVoting.SetSubmarineVotingText(starter, info, transferItems, type); subVoting.Initialize(starter, type); return subVoting; } @@ -160,19 +160,21 @@ namespace Barotrauma } #region Submarine Voting - private void SetSubmarineVotingText(Client starter, SubmarineInfo info, VoteType type) + + private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; string characterRichString = $"‖color:{nameColor.R},{nameColor.G},{nameColor.B}‖{name}‖color:end‖"; string submarineRichString = $"‖color:{SubmarineColor.R},{SubmarineColor.G},{SubmarineColor.B}‖{info.DisplayName}‖color:end‖"; - + string tag = string.Empty; LocalizedString text = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - text = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + tag = transferItems ? "submarinepurchaseandswitchwithitemsvote" : "submarinepurchaseandswitchvote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[amount]", info.Price.ToString()), @@ -189,7 +191,8 @@ namespace Barotrauma int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - text = TextManager.GetWithVariables("submarineswitchfeevote", + tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[locationname]", endLocation.Name), @@ -198,13 +201,13 @@ namespace Barotrauma } else { - text = TextManager.GetWithVariables("submarineswitchnofeevote", + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString)); } break; } - votingOnText = RichString.Rich(text); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 9d9cb8ac5..6a926c4da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -202,6 +202,8 @@ namespace Barotrauma public static bool CancelQuickStart; #endif + public static ChatMode ActiveChatMode { get; set; } = ChatMode.Radio; + public GameMain(string[] args) { Content.RootDirectory = "Content"; @@ -280,9 +282,9 @@ namespace Barotrauma screen.OnFileDropped(filePath, extension); } - public void ApplyGraphicsSettings() + public void ApplyGraphicsSettings(bool recalculateFontsAndStyles = false) { - void updateConfig() + static void updateConfig() { var config = GameSettings.CurrentConfig; config.Graphics.Width = GraphicsWidth; @@ -321,6 +323,12 @@ namespace Barotrauma defaultViewport = GraphicsDevice.Viewport; + if (recalculateFontsAndStyles) + { + GUIStyle.RecalculateFonts(); + GUIStyle.RecalculateSizeRestrictions(); + } + ResolutionChanged?.Invoke(); } @@ -546,6 +554,10 @@ namespace Barotrauma yield return CoroutineStatus.Running; +#if DEBUG + LevelGenerationParams.CheckValidity(); +#endif + MainMenuScreen.Select(); foreach (Identifier steamError in SteamManager.InitializationErrors) @@ -920,7 +932,7 @@ namespace Barotrauma updateCount++; sw.Stop(); - PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); + PerformanceCounter.AddElapsedTicks("Update", sw.ElapsedTicks); PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } @@ -939,6 +951,23 @@ namespace Barotrauma Timing.Accumulator = 0.0f; } + private void FixRazerCortex() + { +#if WINDOWS + //Razer Cortex's overlay is broken. + //For whatever reason, it messes up the blendstate and, + //because MonoGame reasonably assumes that you don't need + //to touch it if you're setting it to the exact same one + //you were already using, it doesn't fix Razer's mess. + //Therefore, we need to change the blendstate TWICE: + //once to force MonoGame to change it, and then again to + //use the blendstate we actually want. + var oldBlendState = GraphicsDevice.BlendState; + GraphicsDevice.BlendState = oldBlendState == BlendState.Opaque ? BlendState.NonPremultiplied : BlendState.Opaque; + GraphicsDevice.BlendState = oldBlendState; +#endif + } + /// /// This is called when the game should draw itself. /// @@ -946,7 +975,9 @@ namespace Barotrauma { Stopwatch sw = new Stopwatch(); sw.Start(); - + + FixRazerCortex(); + double deltaTime = gameTime.ElapsedGameTime.TotalSeconds; if (Timing.FrameLimit > 0) @@ -1003,7 +1034,7 @@ namespace Barotrauma } sw.Stop(); - PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); + PerformanceCounter.AddElapsedTicks("Draw", sw.ElapsedTicks); PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } @@ -1039,7 +1070,7 @@ namespace Barotrauma } // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) + if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); spCampaign.CargoManager.ClearSoldItemsProjSpecific(); @@ -1164,7 +1195,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportgithubform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - UserData = "https://github.com/Regalis11/Barotrauma/issues/new?template=bug_report.md", + UserData = "https://github.com/Regalis11/Barotrauma/issues/new/choose", OnClicked = (btn, userdata) => { ShowOpenUrlInWebBrowserPrompt(userdata as string); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 439e43f96..a4f756646 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -81,7 +81,6 @@ namespace Barotrauma : this(isSinglePlayer) { AddCharacterElements(element); - ActiveOrdersElement = element.GetChildElement("activeorders"); } partial void InitProjectSpecific() @@ -148,9 +147,10 @@ namespace Barotrauma string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); // add to local history ChatBox.ChatManager.Store(text); - WifiComponent headset = null; - ChatMessageType messageType = - ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled, out headset)) ? ChatMessageType.Radio : ChatMessageType.Default; + bool isUsingRadioMode = GameMain.ActiveChatMode == ChatMode.Radio; + bool containsRadioCommand = msgCommand == "r" || msgCommand == "radio"; + bool canUseRadio = ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent headset); + ChatMessageType messageType = ((isUsingRadioMode && msgCommand == "") || containsRadioCommand) && canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; AddSinglePlayerChatMessage( Character.Controlled.Info.Name, msg, messageType, @@ -1554,40 +1554,9 @@ namespace Barotrauma { ChatBox.Update(deltaTime); ChatBox.InputBox.Visible = Character.Controlled != null; - - if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null) + if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null && !ChatBox.InputBox.Selected) { - if (PlayerInput.KeyHit(InputType.Chat) && !ChatBox.InputBox.Selected) - { - ChatBox.InputBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.DarkGreen, 0.5f); - if (!ChatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); - } - - if (PlayerInput.KeyHit(InputType.RadioChat) && !ChatBox.InputBox.Selected) - { - if (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100) - { - ChatBox.InputBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); - if (!ChatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - - if (!ChatBox.InputBox.Text.StartsWith(ChatBox.RadioChatString)) - { - ChatBox.InputBox.Text = ChatBox.RadioChatString; - } - ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); - } - } + ChatBox.ApplySelectionInputs(); } } @@ -1609,7 +1578,7 @@ namespace Barotrauma { if (character == Character.Controlled && crewList.SelectedComponent != characterComponent) { - crewList.Select(character, force: true); + crewList.Select(character, GUIListBox.Force.Yes); } // Icon colors might change based on the target so we check if they need to be updated if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) @@ -3661,9 +3630,9 @@ namespace Barotrauma crewList.ClearChildren(); } - public void Save(XElement parentElement) + public XElement Save(XElement parentElement) { - XElement element = new XElement("crew"); + var element = new XElement("crew"); for (int i = 0; i < characterInfos.Count; i++) { var ci = characterInfos[i]; @@ -3674,8 +3643,8 @@ namespace Barotrauma infoElement.Add(new XAttribute("crewlistindex", ci.CrewListIndex)); if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } - SaveActiveOrders(element); - parentElement.Add(element); + parentElement?.Add(element); + return element; } public static void ClientReadActiveOrders(IReadMessage inc) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 8c8832a05..4a91c9026 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,6 +13,11 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { + if (Owner is Some { Value: var character }) + { + if (!character.IsPlayer) { return; } + } + CampaignMode campaign = GameMain.GameSession?.Campaign; WalletChangedData data = new WalletChangedData { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bde737f0..8fc431ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -587,196 +587,78 @@ namespace Barotrauma //static because we may need to instantiate the campaign if it hasn't been done yet public static void ClientRead(IReadMessage msg) { + NetFlags requiredFlags = (NetFlags)msg.ReadUInt16(); + bool isFirstRound = msg.ReadBoolean(); byte campaignID = msg.ReadByte(); - UInt16 updateID = msg.ReadUInt16(); UInt16 saveID = msg.ReadUInt16(); string mapSeed = msg.ReadString(); - UInt16 currentLocIndex = msg.ReadUInt16(); - UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionCount = msg.ReadByte(); - List selectedMissionIndices = new List(); - for (int i = 0; i < selectedMissionCount; i++) - { - selectedMissionIndices.Add(msg.ReadByte()); - } - - ushort ownedSubCount = msg.ReadUInt16(); - List ownedSubIndices = new List(); - for (int i = 0; i < ownedSubCount; i++) - { - ownedSubIndices.Add(msg.ReadUInt16()); - } - - bool allowDebugTeleport = msg.ReadBoolean(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } - - Dictionary factionReps = new Dictionary(); - byte factionsCount = msg.ReadByte(); - for (int i = 0; i < factionsCount; i++) - { - factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); - } - - bool forceMapUI = msg.ReadBoolean(); - - bool purchasedHullRepairs = msg.ReadBoolean(); - bool purchasedItemRepairs = msg.ReadBoolean(); - bool purchasedLostShuttles = msg.ReadBoolean(); - - byte missionCount = msg.ReadByte(); - var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); - for (int i = 0; i < missionCount; i++) - { - Identifier missionIdentifier = msg.ReadIdentifier(); - byte connectionIndex = msg.ReadByte(); - availableMissions.Add((missionIdentifier, connectionIndex)); - } - - var storeBalances = new Dictionary(); - if (msg.ReadBoolean()) - { - byte storeCount = msg.ReadByte(); - for (int i = 0; i < storeCount; i++) - { - Identifier identifier = msg.ReadIdentifier(); - UInt16 storeBalance = msg.ReadUInt16(); - storeBalances.Add(identifier, storeBalance); - } - } - - var buyCrateItems = ReadPurchasedItems(msg, sender: null); - var subSellCrateItems = ReadPurchasedItems(msg, sender: null); - var purchasedItems = ReadPurchasedItems(msg, sender: null); - var soldItems = ReadSoldItems(msg); - - ushort pendingUpgradeCount = msg.ReadUInt16(); - List pendingUpgrades = new List(); - for (int i = 0; i < pendingUpgradeCount; i++) - { - Identifier upgradeIdentifier = msg.ReadIdentifier(); - UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - Identifier categoryIdentifier = msg.ReadIdentifier(); - UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); - int upgradeLevel = msg.ReadByte(); - if (prefab == null || category == null) { continue; } - pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); - } - - ushort purchasedItemSwapCount = msg.ReadUInt16(); - List purchasedItemSwaps = new List(); - for (int i = 0; i < purchasedItemSwapCount; i++) - { - UInt16 itemToRemoveID = msg.ReadUInt16(); - Identifier itemToInstallIdentifier = msg.ReadIdentifier(); - ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } - purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); - } - - bool hasCharacterData = msg.ReadBoolean(); - CharacterInfo myCharacterInfo = null; - if (hasCharacterData) - { - myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); - } + bool refreshCampaignUI = false; if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure, mapSeed); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); } //server has a newer save file - if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) - { - campaign.PendingSaveID = saveID; - } - - if (NetIdUtils.IdMoreRecent(updateID, campaign.lastUpdateID)) - { - campaign.SuppressStateSending = true; - campaign.IsFirstRound = isFirstRound; + if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { campaign.PendingSaveID = saveID; } + campaign.IsFirstRound = isFirstRound; - //we need to have the latest save file to display location/mission/store - if (campaign.LastSaveID == saveID) + if (requiredFlags.HasFlag(NetFlags.Misc)) + { + DebugConsole.Log("Received campaign update (Misc)"); + UInt16 id = msg.ReadUInt16(); + bool purchasedHullRepairs = msg.ReadBoolean(); + bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); + if (ShouldApply(NetFlags.Misc, id, requireUpToDateSave: false)) + { + refreshCampaignUI = campaign.PurchasedHullRepairs != purchasedHullRepairs || + campaign.PurchasedItemRepairs != purchasedItemRepairs || + campaign.PurchasedLostShuttles != purchasedLostShuttles; + campaign.PurchasedHullRepairs = purchasedHullRepairs; + campaign.PurchasedItemRepairs = purchasedItemRepairs; + campaign.PurchasedLostShuttles = purchasedLostShuttles; + } + } + + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) + { + DebugConsole.Log("Received campaign update (MapAndMissions)"); + UInt16 id = msg.ReadUInt16(); + bool forceMapUI = msg.ReadBoolean(); + bool allowDebugTeleport = msg.ReadBoolean(); + UInt16 currentLocIndex = msg.ReadUInt16(); + UInt16 selectedLocIndex = msg.ReadUInt16(); + + byte missionCount = msg.ReadByte(); + var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); + for (int i = 0; i < missionCount; i++) + { + Identifier missionIdentifier = msg.ReadIdentifier(); + byte connectionIndex = msg.ReadByte(); + availableMissions.Add((missionIdentifier, connectionIndex)); + } + + byte selectedMissionCount = msg.ReadByte(); + List selectedMissionIndices = new List(); + for (int i = 0; i < selectedMissionCount; i++) + { + selectedMissionIndices.Add(msg.ReadByte()); + } + + if (ShouldApply(NetFlags.MapAndMissions, id, requireUpToDateSave: true)) { campaign.ForceMapUI = forceMapUI; - - UpgradeStore.WaitForServerUpdate = false; - + campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - campaign.Map.SelectMission(selectedMissionIndices); - - GameMain.GameSession.OwnedSubmarines.Clear(); - foreach (int ownedSubIndex in ownedSubIndices) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) - { - GameMain.GameSession.OwnedSubmarines.Add(sub); - } - } - - campaign.Map.AllowDebugTeleport = allowDebugTeleport; - campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); - campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); - campaign.CargoManager.SetPurchasedItems(purchasedItems); - campaign.CargoManager.SetSoldItems(soldItems); - foreach (var balance in storeBalances) - { - if (campaign.Map.CurrentLocation.GetStore(balance.Key) is { } store) - { - store.Balance = balance.Value; - } - } - campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); - foreach (var purchasedItemSwap in purchasedItemSwaps) - { - if (purchasedItemSwap.ItemToInstall == null) - { - campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); - } - else - { - campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); - } - } - foreach (Item item in Item.ItemList) - { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) - { - item.PendingItemSwap = null; - } - } - - foreach (var (identifier, rep) in factionReps) - { - Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); - if (faction?.Reputation != null) - { - faction.Reputation.SetReputation(rep); - } - else - { - DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); - } - } - - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - foreach (var availableMission in availableMissions) { MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); @@ -800,36 +682,269 @@ namespace Barotrauma campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } - - GameMain.NetLobbyScreen.ToggleCampaignMode(true); - } - - bool shouldRefresh = campaign.PurchasedHullRepairs != purchasedHullRepairs || - campaign.PurchasedItemRepairs != purchasedItemRepairs || - campaign.PurchasedLostShuttles != purchasedLostShuttles; - - campaign.PurchasedHullRepairs = purchasedHullRepairs; - campaign.PurchasedItemRepairs = purchasedItemRepairs; - campaign.PurchasedLostShuttles = purchasedLostShuttles; - - if (shouldRefresh) - { - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - - if (myCharacterInfo != null) - { - GameMain.Client.CharacterInfo = myCharacterInfo; - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + campaign.Map.SelectMission(selectedMissionIndices); + ReadStores(msg, apply: true); } else { - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + ReadStores(msg, apply: false); + } + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + DebugConsole.Log("Received campaign update (SubList)"); + UInt16 id = msg.ReadUInt16(); + ushort ownedSubCount = msg.ReadUInt16(); + List ownedSubIndices = new List(); + for (int i = 0; i < ownedSubCount; i++) + { + ownedSubIndices.Add(msg.ReadUInt16()); } - campaign.lastUpdateID = updateID; - campaign.SuppressStateSending = false; + if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false)) + { + GameMain.GameSession.OwnedSubmarines.Clear(); + foreach (int ownedSubIndex in ownedSubIndices) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) + { + GameMain.GameSession.OwnedSubmarines.Add(sub); + } + } + } } + + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) + { + DebugConsole.Log("Received campaign update (UpgradeManager)"); + UInt16 id = msg.ReadUInt16(); + + ushort pendingUpgradeCount = msg.ReadUInt16(); + List pendingUpgrades = new List(); + for (int i = 0; i < pendingUpgradeCount; i++) + { + Identifier upgradeIdentifier = msg.ReadIdentifier(); + UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); + Identifier categoryIdentifier = msg.ReadIdentifier(); + UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); + int upgradeLevel = msg.ReadByte(); + if (prefab == null || category == null) { continue; } + pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); + } + + ushort purchasedItemSwapCount = msg.ReadUInt16(); + List purchasedItemSwaps = new List(); + for (int i = 0; i < purchasedItemSwapCount; i++) + { + UInt16 itemToRemoveID = msg.ReadUInt16(); + Identifier itemToInstallIdentifier = msg.ReadIdentifier(); + ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } + purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + } + + if (!Submarine.Unloading && !(Submarine.MainSub is { Loading: true }) && + ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) + { + UpgradeStore.WaitForServerUpdate = false; + campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); + campaign.UpgradeManager.PurchasedUpgrades.Clear(); + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); + } + else + { + campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); + } + } + foreach (Item item in Item.ItemList.ToList()) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + item.PendingItemSwap = null; + } + } + } + } + + + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInBuyCrate)"); + UInt16 id = msg.ReadUInt16(); + var buyCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate)"); + UInt16 id = msg.ReadUInt16(); + var subSellCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) + { + DebugConsole.Log("Received campaign update (PuchasedItems)"); + UInt16 id = msg.ReadUInt16(); + var purchasedItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetPurchasedItems(purchasedItems); + campaign.SetLastUpdateIdForFlag(NetFlags.PurchasedItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.SoldItems)) + { + DebugConsole.Log("Received campaign update (SoldItems)"); + UInt16 id = msg.ReadUInt16(); + var soldItems = ReadSoldItems(msg); + if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetSoldItems(soldItems); + campaign.SetLastUpdateIdForFlag(NetFlags.SoldItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + DebugConsole.Log("Received campaign update (Reputation)"); + UInt16 id = msg.ReadUInt16(); + float? reputation = null; + if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } + Dictionary factionReps = new Dictionary(); + byte factionsCount = msg.ReadByte(); + for (int i = 0; i < factionsCount; i++) + { + factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); + } + if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) + { + if (reputation.HasValue) + { + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + foreach (var (identifier, rep) in factionReps) + { + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); + if (faction?.Reputation != null) + { + faction.Reputation.SetReputation(rep); + } + else + { + DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); + } + } + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + DebugConsole.Log("Received campaign update (CharacterInfo)"); + UInt16 id = msg.ReadUInt16(); + bool hasCharacterData = msg.ReadBoolean(); + CharacterInfo myCharacterInfo = null; + if (hasCharacterData) + { + myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); + } + if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) + { + if (myCharacterInfo != null) + { + GameMain.Client.CharacterInfo = myCharacterInfo; + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + } + else + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + } + } + + campaign.SuppressStateSending = true; + //we need to have the latest save file to display location/mission/store + if (campaign.LastSaveID == saveID) + { + GameMain.NetLobbyScreen.ToggleCampaignMode(true); + } + if (refreshCampaignUI) + { + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + campaign.SuppressStateSending = false; + + bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave) + { + if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) && + (!requireUpToDateSave || saveID == campaign.LastSaveID)) + { + campaign.SetLastUpdateIdForFlag(flag, id); + return true; + } + else + { + return false; + } + } + + void ReadStores(IReadMessage msg, bool apply) + { + var storeBalances = new Dictionary(); + if (msg.ReadBoolean()) + { + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier identifier = msg.ReadIdentifier(); + UInt16 storeBalance = msg.ReadUInt16(); + storeBalances.Add(identifier, storeBalance); + } + } + if (apply) + { + foreach (var balance in storeBalances) + { + if (campaign.Map?.CurrentLocation?.GetStore(balance.Key) is { } store) + { + store.Balance = balance.Value; + } + } + } + } + } public void ClientReadCrew(IReadMessage msg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 53ba428fb..065bfeb8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -35,11 +35,7 @@ namespace Barotrauma } } - if (CrewManager.ChatBox != null) - { - CrewManager.ChatBox.Update(deltaTime); - } - + CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } @@ -58,12 +54,12 @@ namespace Barotrauma /// /// Instantiates a new single player campaign /// - private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - map = new Map(this, mapSeed, settings); Settings = settings; + map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) @@ -79,7 +75,7 @@ namespace Barotrauma /// /// Loads a previously saved single player campaign from XML /// - private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; @@ -87,14 +83,15 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); break; case "crew": GameMain.GameSession.CrewManager = new CrewManager(subElement, true); + ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "map": - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); break; case "metadata": CampaignMetadata = new CampaignMetadata(this, subElement); @@ -162,21 +159,14 @@ namespace Barotrauma /// /// Start a completely new single player campaign /// - public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) - { - var campaign = new SinglePlayerCampaign(mapSeed, settings); - return campaign; - } + public static SinglePlayerCampaign StartNew(string mapSeed, CampaignSettings startingSettings) => new SinglePlayerCampaign(mapSeed, startingSettings); /// /// Load a previously saved single player campaign from xml /// /// /// - public static SinglePlayerCampaign Load(XElement element) - { - return new SinglePlayerCampaign(element); - } + public static SinglePlayerCampaign Load(XElement element) => new SinglePlayerCampaign(element); private void InitUI() { @@ -242,11 +232,10 @@ namespace Barotrauma crewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); - if (petsElement != null) - { - PetBehavior.LoadPets(petsElement); - } - CrewManager.LoadActiveOrders(); + LoadPets(); + LoadActiveOrders(); + + CargoManager.InitPurchasedIDCards(); GUI.DisableSavingIndicatorDelayed(); } @@ -461,41 +450,8 @@ namespace Barotrauma if (success) { - if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) - { - Submarine.MainSub = leavingSub; - GameMain.GameSession.Submarine = leavingSub; - GameMain.GameSession.SubmarineInfo = leavingSub.Info; - leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); - var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); - foreach (Submarine sub in subsToLeaveBehind) - { - GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); - LinkedSubmarine.CreateDummy(leavingSub, sub); - } - } - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - - if (PendingSubmarineSwitch != null) - { - SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - - for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) - { - GameMain.GameSession.OwnedSubmarines[i] = previousSub; - break; - } - } - } - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - PendingSubmarineSwitch = null; } else { @@ -766,11 +722,10 @@ namespace Barotrauma c.Info.SaveOrderData(); } - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); - modeElement.Add(petsElement); + SavePets(modeElement); + var crewManagerElement = CrewManager.Save(modeElement); + SaveActiveOrders(crewManagerElement); - CrewManager.Save(modeElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 2f9e0a46f..76cb87ce0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -102,29 +102,11 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Watchman"); GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - var revolver = FindOrGiveItem(captain, "revolver".ToIdentifier()); - revolver.Unequip(captain); - captain.Inventory.RemoveItem(revolver); - - var captainscap = - captain.Inventory.FindItemByIdentifier("captainscap1".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainscap2".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainscap3".ToIdentifier()); - - if (captainscap != null) + foreach (Item item in captain.Inventory.AllItemsMod) { - captainscap.Unequip(captain); - captain.Inventory.RemoveItem(captainscap); - } - - var captainsuniform = - captain.Inventory.FindItemByIdentifier("captainsuniform1".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainsuniform2".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainsuniform3".ToIdentifier()); - if (captainsuniform != null) - { - captainsuniform.Unequip(captain); - captain.Inventory.RemoveItem(captainsuniform); + if (item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } + item.Unequip(captain); + captain.Inventory.RemoveItem(item); } var steerOrder = OrderPrefab.Prefabs["steer"]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 2a0c3ff52..41a8238bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -105,21 +105,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); doctor = Character.Controlled; - var bandages = FindOrGiveItem(doctor, "antibleeding1".ToIdentifier()); - bandages.Unequip(doctor); - doctor.Inventory.RemoveItem(bandages); - - var syringegun = FindOrGiveItem(doctor, "syringegun".ToIdentifier()); - syringegun.Unequip(doctor); - doctor.Inventory.RemoveItem(syringegun); - - var antibiotics = FindOrGiveItem(doctor, "antibiotics".ToIdentifier()); - antibiotics.Unequip(doctor); - doctor.Inventory.RemoveItem(antibiotics); - - var morphine = FindOrGiveItem(doctor, "antidama1".ToIdentifier()); - morphine.Unequip(doctor); - doctor.Inventory.RemoveItem(morphine); + foreach (Item item in doctor.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } + item.Unequip(doctor); + doctor.Inventory.RemoveItem(item); + } doctor_suppliesCabinet = Item.ItemList.Find(i => i.HasTag("doctor_suppliescabinet"))?.GetComponent(); doctor_medBayCabinet = Item.ItemList.Find(i => i.HasTag("doctor_medbaycabinet"))?.GetComponent(); @@ -260,7 +251,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2.0f); }*/ - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Medical supplies objective do { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index e55e315fb..b95fa6f31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -131,9 +131,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); engineer = Character.Controlled; - var toolbelt = FindOrGiveItem(engineer, "toolbelt".ToIdentifier()); - toolbelt.Unequip(engineer); - engineer.Inventory.RemoveItem(toolbelt); + foreach (Item item in engineer.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } + item.Unequip(engineer); + engineer.Inventory.RemoveItem(item); + } var repairOrder = OrderPrefab.Prefabs["repairsystems"]; engineer_repairIcon = repairOrder.SymbolSprite; @@ -278,7 +281,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_equipmentObjectiveSensor.MotionDetected); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Equipment"), ChatMessageType.Radio, null); yield return new WaitForSeconds(0.5f, false); - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Retrieve equipment bool firstSlotRemoved = false; bool secondSlotRemoved = false; bool thirdSlotRemoved = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index dfe1fca82..06d05b708 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -160,13 +160,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); mechanic = Character.Controlled; - var toolbelt = FindOrGiveItem(mechanic, "toolbelt".ToIdentifier()); - toolbelt.Unequip(mechanic); - mechanic.Inventory.RemoveItem(toolbelt); - - var crowbar = FindOrGiveItem(mechanic, "crowbar".ToIdentifier()); - crowbar.Unequip(mechanic); - mechanic.Inventory.RemoveItem(crowbar); + foreach (Item item in mechanic.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } + item.Unequip(mechanic); + mechanic.Inventory.RemoveItem(item); + } var repairOrder = OrderPrefab.Prefabs["repairsystems"]; mechanic_repairIcon = repairOrder.SymbolSprite; @@ -297,7 +296,10 @@ namespace Barotrauma.Tutorials public override void Update(float deltaTime) { - mechanic_brokenhull_1.WaterVolume = MathHelper.Clamp(mechanic_brokenhull_1.WaterVolume, 0, mechanic_brokenhull_1.Volume * 0.85f); + if (mechanic_brokenhull_1 != null) + { + mechanic_brokenhull_1.WaterVolume = MathHelper.Clamp(mechanic_brokenhull_1.WaterVolume, 0, mechanic_brokenhull_1.Volume * 0.85f); + } base.Update(deltaTime); } @@ -334,7 +336,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(0.0f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Equipment"), ChatMessageType.Radio, null); do { yield return null; } while (!mechanic_equipmentObjectiveSensor.MotionDetected); - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Equipment & inventory objective SetHighlight(mechanic_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -377,7 +379,7 @@ namespace Barotrauma.Tutorials // Room 3 do { yield return null; } while (!mechanic_weldingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Welding objective + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Welding objective do { if (!mechanic.HasEquippedItem("divingmask".ToIdentifier())) @@ -413,7 +415,7 @@ namespace Barotrauma.Tutorials } } while (mechanic_workingPump.FlowPercentage >= 0 || !mechanic_workingPump.IsActive); // Highlight until draining SetHighlight(mechanic_workingPump.Item, false); - do { yield return null; } while (mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained + do { yield return null; } while (mechanic_brokenhull_1 != null && mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained RemoveCompletedObjective(3); GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective3"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index b2514450f..dd0f49fe5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -141,36 +141,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); officer = Character.Controlled; - var handcuffs = FindOrGiveItem(officer, "handcuffs".ToIdentifier()); - handcuffs.Unequip(officer); - officer.Inventory.RemoveItem(handcuffs); - - var stunbaton = FindOrGiveItem(officer, "stunbaton".ToIdentifier()); - stunbaton.Unequip(officer); - officer.Inventory.RemoveItem(stunbaton); - - var smg = FindOrGiveItem(officer, "smg".ToIdentifier()); - smg.Unequip(officer); - officer.Inventory.RemoveItem(smg); - - var divingknife = FindOrGiveItem(officer, "divingknife".ToIdentifier()); - divingknife.Unequip(officer); - officer.Inventory.RemoveItem(divingknife); - - var steroids = FindOrGiveItem(officer, "steroids".ToIdentifier()); - steroids.Unequip(officer); - officer.Inventory.RemoveItem(steroids); - - var ballistichelmet = - officer.Inventory.FindItemByIdentifier("ballistichelmet1".ToIdentifier()) ?? - officer.Inventory.FindItemByIdentifier("ballistichelmet2".ToIdentifier()) ?? - FindOrGiveItem(officer, "ballistichelmet3".ToIdentifier()); - ballistichelmet.Unequip(officer); - officer.Inventory.RemoveItem(ballistichelmet); - - var bodyarmor = FindOrGiveItem(officer, "bodyarmor".ToIdentifier()); - bodyarmor.Unequip(officer); - officer.Inventory.RemoveItem(bodyarmor); + foreach (Item item in officer.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } + item.Unequip(officer); + officer.Inventory.RemoveItem(item); + } var gunOrder = OrderPrefab.Prefabs["operateweapons"]; officer_gunIcon = gunOrder.SymbolSprite; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index fd0a96f1d..7d72179b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -1,10 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Tutorials { @@ -106,7 +103,7 @@ namespace Barotrauma.Tutorials Character.Controlled = character; character.GiveJobItems(null); - var idCard = character.Inventory.FindItemByIdentifier("idcard".ToIdentifier()); + var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier()); if (idCard == null) { DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); @@ -228,6 +225,8 @@ namespace Barotrauma.Tutorials { CoroutineManager.StopCoroutines(tutorialCoroutine); } + GUI.PreventPauseMenuToggle = false; + ContentRunning = false; infoBox = null; } else if (Character.Controlled.IsDead) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 003816dae..4db03f290 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -28,7 +28,7 @@ namespace Barotrauma UserListData = "ReadyUserList", ReadySpriteData = "ReadySprite"; - private int lastSecond; + private int lastSecond = 1; private GUIMessageBox? msgBox; private GUIMessageBox? resultsBox; @@ -44,7 +44,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); - new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUIStyle.Orange) { UserData = TimerData }; + new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData }; // Yes msgBox.Buttons[0].OnClicked = delegate @@ -116,17 +116,18 @@ namespace Barotrauma private void UpdateBar() { + double elapsedTime = (DateTime.Now - startTime).TotalSeconds; if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox)) { if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) { - bar.BarSize = time / endTime; + bar.BarSize = (float)(elapsedTime / (endTime - startTime).TotalSeconds); } } // play click sound after a second has passed - int second = (int) Math.Ceiling(time); - if (second < lastSecond) + int second = (int)Math.Ceiling(elapsedTime); + if (second > lastSecond) { if (msgBox != null && !msgBox.Closed) { @@ -156,7 +157,8 @@ namespace Barotrauma bool isOwn = false; byte authorId = 0; - float duration = inc.ReadSingle(); + long startTime = inc.ReadInt64(); + long endTime = inc.ReadInt64(); string author = inc.ReadString(); bool hasAuthor = inc.ReadBoolean(); @@ -173,7 +175,9 @@ namespace Barotrauma clients.Add(inc.ReadByte()); } - ReadyCheck rCheck = new ReadyCheck(clients, duration); + ReadyCheck rCheck = new ReadyCheck(clients, + DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, + DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime); crewManager.ActiveReadyCheck = rCheck; if (isOwn) @@ -192,12 +196,10 @@ namespace Barotrauma } break; case ReadyCheckState.Update: - float time = inc.ReadSingle(); ReadyStatus newState = (ReadyStatus) inc.ReadByte(); byte targetId = inc.ReadByte(); if (crewManager.ActiveReadyCheck != null) { - crewManager.ActiveReadyCheck.time = time; crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 4746f0e9f..490516616 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -64,7 +64,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; public static int Spacing; - public static int HideButtonWidth; private Layout layout; public Layout CurrentLayout @@ -77,64 +76,11 @@ namespace Barotrauma SetSlotPositions(layout); } } - public bool Hidden { get; set; } - - private bool hidePersonalSlots; - private float hidePersonalSlotsState; - private GUIButton hideButton; + private Rectangle personalSlotArea; - public bool HidePersonalSlots - { - get { return hidePersonalSlots; } - } - - public Rectangle PersonalSlotArea - { - get { return personalSlotArea; } - } - - private readonly GUIImage[] indicators = new GUIImage[5]; - private readonly int[] indicatorIndices = new int[5]; - private Vector2 indicatorSpriteSize; - private GUILayoutGroup indicatorGroup; - partial void InitProjSpecific(XElement element) { - Hidden = true; - - hideButton = new GUIButton(new RectTransform(new Point((int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)), HUDLayoutSettings.BottomRightInfoArea.Height), GUI.Canvas) - { AbsoluteOffset = HUDLayoutSettings.CrewArea.Location }, - "", style: "EquipmentToggleButton"); - - indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; - indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUIStyle.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; - - indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); - indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); - indicators[2] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorOutfit"); - indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); - indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); - - indicatorIndices[0] = FindLimbSlot(InvSlotType.OuterClothes); - indicatorIndices[1] = FindLimbSlot(InvSlotType.Card); - indicatorIndices[2] = FindLimbSlot(InvSlotType.InnerClothes); - indicatorIndices[3] = FindLimbSlot(InvSlotType.Head); - indicatorIndices[4] = FindLimbSlot(InvSlotType.Headset); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].CanBeFocused = false; - } - - hideButton.OnClicked += (GUIButton btn, object userdata) => - { - hidePersonalSlots = !hidePersonalSlots; - return true; - }; - hidePersonalSlots = false; - SlotPositions = new Vector2[SlotTypes.Length]; CurrentLayout = Layout.Default; SetSlotPositions(layout); @@ -271,25 +217,6 @@ namespace Barotrauma return false; } - private void SetIndicatorSizes() - { - indicatorGroup.RectTransform.AbsoluteOffset = new Point((int)Math.Round(4 * GUI.Scale), (int)Math.Round(7 * GUI.Scale)); - indicatorGroup.RectTransform.NonScaledSize = new Point(hideButton.Rect.Width - indicatorGroup.RectTransform.AbsoluteOffset.X * 2, hideButton.Rect.Height - indicatorGroup.RectTransform.AbsoluteOffset.Y * 2); - indicatorGroup.AbsoluteSpacing = (int)Math.Ceiling(2 * GUI.Scale); - - int indicatorHeight = (indicatorGroup.RectTransform.NonScaledSize.Y - indicatorGroup.AbsoluteSpacing * (indicators.Length - 1)) / indicators.Length; - int indicatorWidth = (int)(indicatorSpriteSize.X / (indicatorSpriteSize.Y / indicatorHeight)); - - if (HideButtonWidth % 2 != indicatorWidth % 2) indicatorWidth++; - - Point indicatorSize = new Point(indicatorWidth, indicatorHeight); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].RectTransform.NonScaledSize = indicatorSize; - } - } - private void SetSlotPositions(Layout layout) { bool isFourByThree = GUI.IsFourByThree(); @@ -302,13 +229,9 @@ namespace Barotrauma Spacing = (int)(8 * UIScale); } - HideButtonWidth = (int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; - hideButton.Visible = false; - if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -320,7 +243,7 @@ namespace Barotrauma int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); @@ -343,16 +266,6 @@ namespace Barotrauma x += SlotSize.X + Spacing; } } - - if (hideButtonSlotIndex > -1) - { - hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); - hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); - hideButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.BottomRightInfoArea.Left - HideButtonWidth + GUI.IntScaleCeiling(2f), HUDLayoutSettings.BottomRightInfoArea.Y + GUI.IntScaleCeiling(1f)); - hideButton.Visible = Screen.Selected != GameMain.SubEditorScreen || !GameMain.SubEditorScreen.WiringMode; - - SetIndicatorSizes(); - } } break; case Layout.Right: @@ -525,6 +438,7 @@ namespace Barotrauma if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { syncItemsDelay = Math.Max(syncItemsDelay - deltaTime, 0.0f); + doubleClickedItems.Clear(); return; } @@ -532,58 +446,13 @@ namespace Barotrauma bool hoverOnInventory = GUI.MouseOn == null && ((selectedSlot != null && selectedSlot.IsSubSlot) || (DraggingItems.Any() && (DraggingSlot == null || !DraggingSlot.MouseOn()))); - if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; - - if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) - { - if (hideButton.Visible) - { - hideButton.AddToGUIUpdateList(); - hideButton.UpdateManually(deltaTime, alsoChildren: true); - - hidePersonalSlotsState = hidePersonalSlots ? - Math.Min(hidePersonalSlotsState + deltaTime * 5.0f, 1.0f) : - Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); - - bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; - for (int i = 0; i < visualSlots.Length; i++) - { - if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } - if (HidePersonalSlots) - { - if (selectedSlot?.Slot == visualSlots[i]) { selectedSlot = null; } - highlightedSubInventorySlots.RemoveWhere(s => s.Slot == visualSlots[i]); - } - visualSlots[i].IsMoving = personalSlotsMoving; - visualSlots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); - } - } - } + if (CharacterHealth.OpenHealthWindow != null) { hoverOnInventory = true; } if (hoverOnInventory) { HideTimer = 0.5f; } if (HideTimer > 0.0f) { HideTimer -= deltaTime; } UpdateSlotInput(); - //force personal slots open if an item is running out of battery/fuel/oxygen/etc - if (hidePersonalSlots) - { - for (int i = 0; i < visualSlots.Length; i++) - { - var item = slots[i].FirstOrDefault(); - if (item?.OwnInventory != null && item.OwnInventory.Capacity == 1 && PersonalSlots.HasFlag(SlotTypes[i])) - { - var containedItem = item.OwnInventory.AllItems.FirstOrDefault(); - if (containedItem != null && - containedItem.Condition > 0.0f && - containedItem.Condition / containedItem.MaxCondition < 0.15f) - { - hidePersonalSlots = false; - } - } - } - } - hideSubInventories.Clear(); //remove highlighted subinventory slots that can no longer be accessed highlightedSubInventorySlots.RemoveWhere(s => @@ -652,8 +521,6 @@ namespace Barotrauma if (character == Character.Controlled && character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { - UpdateEquipmentIndicators(); - //remove the highlighted slots of other characters' inventories when not grabbing anyone highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); @@ -798,40 +665,6 @@ namespace Barotrauma } } } - - private void UpdateEquipmentIndicators() - { - for (int i = 0; i < indicators.Length; i++) - { - if (indicatorIndices[i] < 0) { continue; } - Item item = slots[indicatorIndices[i]].FirstOrDefault(); - if (item != null) - { - Wearable wearable = item.GetComponent(); - if (wearable != null && wearable.DisplayContainedStatus) - { - float conditionPercentage = item.GetContainedItemConditionPercentage(); - - if (conditionPercentage != -1) - { - indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUIStyle.EquipmentIndicatorRunningOut, GUIStyle.EquipmentIndicatorEquipped); - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorRunningOut; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorEquipped; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorNotEquipped; - } - } - } private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { @@ -931,7 +764,7 @@ namespace Barotrauma // Move the item from the subinventory to the selected container return QuickUseAction.PutToContainer; } - else + else if (character.Inventory.AccessibleWhenAlive || character.Inventory.AccessibleByOwner) { // Take from the subinventory and place it in the character's main inventory if no target container is selected return QuickUseAction.TakeFromContainer; @@ -941,6 +774,7 @@ namespace Barotrauma } else { + bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedConstruction?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && @@ -959,14 +793,16 @@ namespace Barotrauma } else if (character.SelectedBy?.Inventory != null && Character.Controlled == character.SelectedBy && - !character.SelectedBy.Inventory.Locked && + !character.SelectedBy.Inventory.Locked && + (character.SelectedBy.Inventory.AccessibleWhenAlive || character.SelectedBy.Inventory.AccessibleByOwner) && allowInventorySwap) { return QuickUseAction.TakeFromCharacter; } else if (character.HeldItems.Any(i => - i.OwnInventory != null && - ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + i.OwnInventory != null && + /*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ + ((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -1129,11 +965,18 @@ namespace Barotrauma } break; case QuickUseAction.PutToEquippedItem: + foreach (Item heldItem in character.HeldItems) { if (heldItem.OwnInventory == null) { continue; } + //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items + //(in that case, the quick action should just fill up the stack) + bool disallowSwapping = + heldItem.OwnInventory.Capacity == 1 && + heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && + heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: true, allowCombine: false, user: Character.Controlled))) + (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) @@ -1195,11 +1038,6 @@ namespace Barotrauma DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem, SlotTypes[i]); } - - if (hideButton != null && hideButton.Visible && !Locked) - { - hideButton.DrawManually(spriteBatch, alsoChildren: true); - } VisualSlot highlightedQuickUseSlot = null; Rectangle inventoryArea = Rectangle.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 2b372fd27..88bd89496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -43,6 +43,31 @@ namespace Barotrauma.Items.Components corners[2] = center + new Vector2(shadowSize.X, shadowSize.Y) / 2; corners[3] = center + new Vector2(shadowSize.X, -shadowSize.Y) / 2; + if (IsHorizontal) + { + if (item.FlippedX) + { + Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2); + for (int i = 0; i < corners.Length; i++) + { + corners[i].X = itemCenter.X * 2 - corners[i].X; + } + Array.Reverse(corners); + } + } + else + { + if (item.FlippedY) + { + Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2); + for (int i = 0; i < corners.Length; i++) + { + corners[i].Y = itemCenter.Y * 2 - corners[i].Y; + } + Array.Reverse(corners); + } + } + return corners; } @@ -66,6 +91,9 @@ namespace Barotrauma.Items.Components rect.Height = (int)(rect.Height * (1.0f - openState)); } + //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed + //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion + bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f; if (Window.Height > 0 && Window.Width > 0) { if (IsHorizontal) @@ -88,7 +116,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2)); + convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); } } } @@ -112,7 +140,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2)); + convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); } } } @@ -127,7 +155,7 @@ namespace Barotrauma.Items.Components else { convexHull.Enabled = true; - convexHull.SetVertices(GetConvexHullCorners(rect)); + convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); } } @@ -160,69 +188,68 @@ namespace Barotrauma.Items.Components if (stuck > 0.0f && weldedSprite != null) { Vector2 weldSpritePos = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2.0f) + shakePos; - if (item.Submarine != null) weldSpritePos += item.Submarine.DrawPosition; + if (item.Submarine != null) { weldSpritePos += item.Submarine.DrawPosition; } weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale); } - if (openState >= 1.0f) - { - return; - } + if (openState >= 1.0f) { return; } + Vector2 pos; if (IsHorizontal) { - Vector2 pos = new Vector2(item.Rect.X, item.Rect.Y - item.Rect.Height / 2) + shakePos; - if (item.Submarine != null) pos += item.Submarine.DrawPosition; - pos.Y = -pos.Y; - - if (brokenSprite == null || !IsBroken) - { - spriteBatch.Draw(doorSprite.Texture, pos, - new Rectangle((int) (doorSprite.SourceRect.X + doorSprite.size.X * openState), - (int) doorSprite.SourceRect.Y, - (int) (doorSprite.size.X * (1.0f - openState)), (int) doorSprite.size.Y), - color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); - } - - if (brokenSprite != null && item.Health < item.MaxCondition) - { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.MaxCondition) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; - spriteBatch.Draw(brokenSprite.Texture, pos, - new Rectangle((int)(brokenSprite.SourceRect.X + brokenSprite.size.X * openState), brokenSprite.SourceRect.Y, - (int)(brokenSprite.size.X * (1.0f - openState)), (int)brokenSprite.size.Y), - color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, SpriteEffects.None, - brokenSprite.Depth); - } + pos = new Vector2(item.Rect.X, item.Rect.Y - item.Rect.Height / 2); + if (item.FlippedX) { pos.X += (int)(doorSprite.size.X * item.Scale * openState); } } else { - Vector2 pos = new Vector2(item.Rect.Center.X, item.Rect.Y) + shakePos; - if (item.Submarine != null) pos += item.Submarine.DrawPosition; - pos.Y = -pos.Y; - - if (brokenSprite == null || !IsBroken) - { - spriteBatch.Draw(doorSprite.Texture, pos, - new Rectangle(doorSprite.SourceRect.X, - (int) (doorSprite.SourceRect.Y + doorSprite.size.Y * openState), - (int) doorSprite.size.X, (int) (doorSprite.size.Y * (1.0f - openState))), - color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); - } - - if (brokenSprite != null && item.Health < item.MaxCondition) - { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition, 1.0f) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; - spriteBatch.Draw(brokenSprite.Texture, pos, - new Rectangle(brokenSprite.SourceRect.X, (int)(brokenSprite.SourceRect.Y + brokenSprite.size.Y * openState), - (int)brokenSprite.size.X, (int)(brokenSprite.size.Y * (1.0f - openState))), - color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, SpriteEffects.None, brokenSprite.Depth); - } + pos = new Vector2(item.Rect.Center.X, item.Rect.Y); + if (item.FlippedY) { pos.Y -= (int)(doorSprite.size.Y * item.Scale * openState); } } + + pos += shakePos; + if (item.Submarine != null) { pos += item.Submarine.DrawPosition; } + pos.Y = -pos.Y; + + if (brokenSprite == null || !IsBroken) + { + spriteBatch.Draw(doorSprite.Texture, pos, + getSourceRect(doorSprite, openState, IsHorizontal), + color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); + } + + if (brokenSprite != null && item.Health < item.MaxCondition) + { + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition) : Vector2.One; + if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } + float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; + spriteBatch.Draw(brokenSprite.Texture, pos, + getSourceRect(brokenSprite, openState, IsHorizontal), + color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, item.SpriteEffects, + brokenSprite.Depth); + } + + static Rectangle getSourceRect(Sprite sprite, float openState, bool horizontal) + { + if (horizontal) + { + return new Rectangle( + (int)(sprite.SourceRect.X + sprite.size.X * openState), + sprite.SourceRect.Y, + (int)(sprite.size.X * (1.0f - openState)), + (int)sprite.size.Y); + } + else + { + return new Rectangle( + sprite.SourceRect.X, + (int)(sprite.SourceRect.Y + sprite.size.Y * openState), + (int)sprite.size.X, + (int)(sprite.size.Y * (1.0f - openState))); + } + } } partial void OnFailedToOpen() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index d903016ad..8428d53ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -321,7 +321,7 @@ namespace Barotrauma.Items.Components { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Int) { IntValue = defaultValue }; + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), NumberType.Int) { IntValue = defaultValue }; return input; } @@ -329,7 +329,7 @@ namespace Barotrauma.Items.Components { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = defaultValue, DecimalsToDisplay = 2 }; + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), NumberType.Float) { FloatValue = defaultValue, DecimalsToDisplay = 2 }; return input; } @@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components for (var i = 0; i < values.Length; i++) { float value = values[i]; - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f / values.Length, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f / values.Length, 1f), layout.RectTransform), NumberType.Float) { FloatValue = value, DecimalsToDisplay = 2, MinValueFloat = min, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index 23083323d..bd2b6da47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -116,12 +116,6 @@ namespace Barotrauma.Items.Components loadAttachments(Attachments, disguisedBeardElement, WearableType.Beard); loadAttachments(Attachments, disguisedMoustacheElement, WearableType.Moustache); loadAttachments(Attachments, disguisedHairElement, WearableType.Hair); - - loadAttachments(Attachments, - characterInfo.OmitJobInPortraitClothing - ? JobPrefab.NoJobElement?.GetChildElement("PortraitClothing") - : JobPrefab?.ClothingElement, - WearableType.JobIndicator); } HairColor = hairColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index a189f5470..9b287cbd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -203,7 +203,7 @@ namespace Barotrauma.Items.Components private float lastMuffleCheckTime; private ItemSound loopingSound; private SoundChannel loopingSoundChannel; - private List playingOneshotSoundChannels = new List(); + private readonly List playingOneshotSoundChannels = new List(); public ItemComponent ReplacedBy; public ItemComponent GetReplacementOrThis() @@ -211,13 +211,16 @@ namespace Barotrauma.Items.Components return ReplacedBy?.GetReplacementOrThis() ?? this; } + public bool NeedsSoundUpdate() + { + if (hasSoundsOfType[(int)ActionType.Always]) { return true; } + if (loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { return true; } + if (playingOneshotSoundChannels.Count > 0) { return true; } + return false; + } + public void UpdateSounds() { - if (!isActive || item.Condition <= 0.0f) - { - StopSounds(ActionType.OnActive); - } - if (loopingSound != null && loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { if (Timing.TotalTime > lastMuffleCheckTime + 0.2f) @@ -280,6 +283,7 @@ namespace Barotrauma.Items.Components loopingSound.RoundSound.GetRandomFrequencyMultiplier(), SoundPlayer.ShouldMuffleSound(Character.Controlled, item.WorldPosition, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; + item.CheckNeedsSoundUpdate(this); //TODO: tweak loopingSoundChannel.Near = loopingSound.Range * 0.4f; loopingSoundChannel.Far = loopingSound.Range; @@ -298,7 +302,6 @@ namespace Barotrauma.Items.Components loopingSound = null; } } - return; } @@ -333,6 +336,7 @@ namespace Barotrauma.Items.Components } PlaySound(matchingSounds[index], item.WorldPosition); + item.CheckNeedsSoundUpdate(this); } } private void PlaySound(ItemSound itemSound, Vector2 position) @@ -401,7 +405,7 @@ namespace Barotrauma.Items.Components float newVolume; try { - newVolume = property.GetFloatValue(this); + newVolume = Math.Min(property.GetFloatValue(this), 1.0f); } catch { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5fe3adde1..b1d894203 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -280,9 +280,9 @@ namespace Barotrauma.Items.Components transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; } - if (Math.Abs(item.Rotation) > 0.01f) + if (Math.Abs(item.RotationRad) > 0.01f) { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); transformedItemPos = Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition; transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 52cf7fef1..337a21274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -56,9 +56,7 @@ namespace Barotrauma.Items.Components } else { - Vector2 pos = item.DrawPosition; - if (item.Submarine != null) { pos -= item.Submarine.DrawPosition; } - Light.Position = pos; + Light.Position = item.Position; } PhysicsBody body = Light.ParentBody; if (body != null) @@ -68,7 +66,7 @@ namespace Barotrauma.Items.Components } else { - Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); + Light.Rotation = -Rotation - item.RotationRad; Light.LightSpriteEffect = item.SpriteEffects; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 85791f95b..52c211c18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -78,7 +79,7 @@ namespace Barotrauma.Items.Components activateButton = new GUIButton(new RectTransform(new Vector2(0.95f, 0.8f), buttonContainer.RectTransform), TextManager.Get("DeconstructorDeconstruct"), style: "DeviceButton") { TextBlock = { AutoScaleHorizontal = true }, - OnClicked = ToggleActive + OnClicked = OnActivateButtonClicked }; inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), TextManager.Get("DeconstructorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) @@ -164,7 +165,7 @@ namespace Barotrauma.Items.Components } } } - activateButton.Enabled = outputsFound; + activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty(); activateButton.Text = TextManager.Get(ActivateButtonText); }; } @@ -236,8 +237,19 @@ namespace Barotrauma.Items.Components inSufficientPowerWarning.Visible = IsActive && !hasPower; } - private bool ToggleActive(GUIButton button, object obj) + private bool OnActivateButtonClicked(GUIButton button, object obj) { + var disallowedItem = inputContainer.Inventory.FindItem(i => !i.AllowDeconstruct, recursive: false); + if (disallowedItem != null && !DeconstructItemsSimultaneously) + { + int index = inputContainer.Inventory.FindIndex(disallowedItem); + if (index >= 0 && index < inputContainer.Inventory.visualSlots.Length) + { + var slot = inputContainer.Inventory.visualSlots[index]; + slot?.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); + } + return true; + } if (GameMain.Client != null) { pendingState = !IsActive; @@ -247,7 +259,6 @@ namespace Barotrauma.Items.Components { SetActive(!IsActive, Character.Controlled); } - return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7af73e740..90849cd19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), paddedItemFrame.RectTransform), style: null) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { selectedItem = userdata as FabricationRecipe; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 87289c772..371d3882b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -333,6 +333,7 @@ namespace Barotrauma.Items.Components GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, o) => { if (o is ItemPrefab prefab) @@ -613,7 +614,7 @@ namespace Barotrauma.Items.Components if (hullData.Distort) { hullData.ReceivedOxygenAmount = Rand.Range(0.0f, 100.0f); - hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); + hullData.ReceivedWaterAmount = Rand.Range(0.0f, 100.0f); } hullData.DistortionTimer = Rand.Range(1.0f, 10.0f); } @@ -681,7 +682,7 @@ namespace Barotrauma.Items.Components var sprite = GUIStyle.UIGlowSolidCircular.Value?.Sprite; float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; - if (sprite != null) + if (sprite != null && ShowHullIntegrity) { Vector2 spriteSize = sprite.size; Rectangle worldBorders = item.Submarine.GetDockedBorders(); @@ -744,11 +745,11 @@ namespace Barotrauma.Items.Components if (key == Keys.Down) { - listBox.SelectNext(true, autoScroll: true); + listBox.SelectNext(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Up) { - listBox.SelectPrevious(true, autoScroll: true); + listBox.SelectPrevious(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Enter) { @@ -782,7 +783,7 @@ namespace Barotrauma.Items.Components if (component.Visible && first) { - listBox.Select(i, force: true, autoScroll: false); + listBox.Select(i, GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled); first = false; } } @@ -1014,13 +1015,13 @@ namespace Barotrauma.Items.Components hullData.HullWaterAmount = 0.0f; foreach (Hull linkedHull in hullData.LinkedHulls) { - hullData.HullWaterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + hullData.HullWaterAmount += WaterDetector.GetWaterPercentage(linkedHull); } hullData.HullWaterAmount /= hullData.LinkedHulls.Count; } else { - hullData.HullWaterAmount = Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + hullData.HullWaterAmount = WaterDetector.GetWaterPercentage(hull); } float gapOpenSum = 0.0f; @@ -1052,8 +1053,8 @@ namespace Barotrauma.Items.Components LocalizedString line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); - Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount); + TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value) + "%"); + Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount / 100.0f); SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } @@ -1188,7 +1189,8 @@ namespace Barotrauma.Items.Components if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) { - if (!RequireWaterDetectors) { waterAmount = hull.WaterPercentage / 100.0f; } + if (!RequireWaterDetectors) { waterAmount = WaterDetector.GetWaterPercentage(hull); } + waterAmount /= 100.0f; if (hullFrame.Rect.Height * waterAmount > 1.0f) { RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); @@ -1327,7 +1329,7 @@ namespace Barotrauma.Items.Components pos.X += inflate; pos.Y += inflate; - sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, MathHelper.ToRadians(item.Rotation), spriteScale, item.SpriteEffects); + sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, item.RotationRad, spriteScale, item.SpriteEffects); void DrawAdditionalSprite(Vector2 basePos, Sprite addSprite, float rotation) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index a3724b8a3..2798830ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components } GuiFrame = selectionUI.GuiFrame; - selectionUI.RefreshSubmarineDisplay(true); + selectionUI.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); IsActive = true; return base.Select(character); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index e50bbcc8d..f3bfe988b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -133,7 +133,6 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { - float rotationRad = MathHelper.ToRadians(item.Rotation); if (FlowPercentage < 0.0f) { foreach (var (position, emitter) in pumpOutEmitters) @@ -142,8 +141,8 @@ namespace Barotrauma.Items.Components //only emit "pump out" particles when underwater Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); - float angle = -rotationRad; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); + float angle = -item.RotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; @@ -163,8 +162,8 @@ namespace Barotrauma.Items.Components foreach (var (position, emitter) in pumpInEmitters) { Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); - float angle = -rotationRad; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); + float angle = -item.RotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 90f0e04b3..0b3583f3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1367,6 +1367,15 @@ namespace Barotrauma.Items.Components pingRadius, prevPingRadius, 250.0f, 150.0f, range, pingStrength, passive); } + if (pingSource.Y - Level.Loaded.BottomPos < range) + { + CreateBlipsForLine( + new Vector2(pingSource.X - range, Level.Loaded.BottomPos), + new Vector2(pingSource.X + range, Level.Loaded.BottomPos), + pingSource, transducerPos, + pingRadius, prevPingRadius, + 250.0f, 150.0f, range, pingStrength, passive); + } List cells = Level.Loaded.GetCells(pingSource, 7); foreach (Voronoi2.VoronoiCell cell in cells) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 2fbf7b38d..d85bca980 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -927,6 +927,8 @@ namespace Barotrauma.Items.Components bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); + ushort userID = msg.ReadUInt16(); + Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; @@ -935,7 +937,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: Entity.FindEntityByID(userID) as Character), "toggle_docking"); } if (autoPilot) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs index de9dc05fe..c0d347358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -40,8 +40,6 @@ namespace Barotrauma.Items.Components } } - private LightComponent lightComponent; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { for (var i = 0; i < GrowableSeeds.Length; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 759f67ad6..a449dc00f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -23,10 +23,10 @@ namespace Barotrauma.Items.Components } #endif - private List particleEmitters = new List(); - private List particleEmitterHitStructure = new List(); - private List particleEmitterHitCharacter = new List(); - private List> particleEmitterHitItem = new List>(); + private readonly List particleEmitters = new List(); + private readonly List particleEmitterHitStructure = new List(); + private readonly List particleEmitterHitCharacter = new List(); + private readonly List<(RelatedItem relatedItem, ParticleEmitter emitter)> particleEmitterHitItem = new List<(RelatedItem relatedItem, ParticleEmitter emitter)>(); private float prevProgressBarState; private Item prevProgressBarTarget = null; @@ -46,10 +46,7 @@ namespace Barotrauma.Items.Components Identifier[] excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifiers", Array.Empty()); if (excludedIdentifiers.Length == 0) { excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifier", Array.Empty()); } - particleEmitterHitItem.Add( - new Pair( - new RelatedItem(identifiers, excludedIdentifiers), - new ParticleEmitter(subElement))); + particleEmitterHitItem.Add((new RelatedItem(identifiers, excludedIdentifiers), new ParticleEmitter(subElement))); break; case "particleemitterhitstructure": particleEmitterHitStructure.Add(new ParticleEmitter(subElement)); @@ -139,11 +136,11 @@ namespace Barotrauma.Items.Components Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition; - foreach (var emitter in particleEmitterHitItem) + foreach ((RelatedItem relatedItem, ParticleEmitter emitter) in particleEmitterHitItem) { - if (!emitter.First.MatchesItem(targetItem)) { continue; } + if (!relatedItem.MatchesItem(targetItem)) { continue; } float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); - emitter.Second.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); + emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); } } #if DEBUG diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 5d3e959c3..156e1afc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -1,11 +1,10 @@ -using System; -using Barotrauma.Networking; +using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -419,7 +418,7 @@ namespace Barotrauma.Items.Components if (!GameMain.IsMultiplayer) { RepairBoost(qteSuccess); } - SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.IncreaseQuantity : GUISoundType.DecreaseQuantity); + SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.Increase : GUISoundType.Decrease); //on failure during cooldown reset cursor to beginning if (!qteSuccess && qteCooldown > 0.0f) { qteTimer = QteDuration; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 36713eab5..9526f7f63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components { if (target == null || target.Removed) { return; } if (target.ParentInventory != null) { return; } + if (source is Limb limb && limb.Removed) { return; } + if (source is Entity e && e.Removed) { return; } Vector2 startPos = GetSourcePos(); startPos.Y = -startPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 93c653f0b..8d5d16766 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components int totalWireCount = 0; foreach (Connection c in panel.Connections) { - totalWireCount += c.Wires.Count(w => w != null); + totalWireCount += c.Wires.Count; } Wire equippedWire = null; @@ -87,8 +87,8 @@ namespace Barotrauma.Items.Components (DraggingConnected.Connections[0] == null && DraggingConnected.Connections[1] == null) || (DraggingConnected.Connections.Contains(c) && DraggingConnected.Connections.Contains(null))) { - int linkIndex = c.FindWireIndex(DraggingConnected.Item); - if (linkIndex > -1 || panel.DisconnectedWires.Contains(DraggingConnected)) + var linkedWire = c.FindWireByItem(DraggingConnected.Item); + if (linkedWire != null || panel.DisconnectedWires.Contains(DraggingConnected)) { Inventory.DraggingItems.Clear(); Inventory.DraggingItems.Add(DraggingConnected.Item); @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components c.DrawWires(spriteBatch, panel, rightPos, rightWirePos, mouseInRect, equippedWire, wireInterval); } rightPos.Y += connectorIntervalLeft; - rightWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; + rightWirePos.Y += c.Wires.Count * wireInterval; } else { @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components c.DrawWires(spriteBatch, panel, leftPos, leftWirePos, mouseInRect, equippedWire, wireInterval); } leftPos.Y += connectorIntervalRight; - leftWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; + leftWirePos.Y += c.Wires.Count * wireInterval; } } } @@ -228,15 +228,15 @@ namespace Barotrauma.Items.Components { float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null || wires[i].Hidden || (DraggingConnected == wires[i] && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } - if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } + if (wire.Hidden || (DraggingConnected == wire && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } + if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } - Connection recipient = wires[i].OtherConnection(this); + Connection recipient = wire.OtherConnection(this); LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; - if (wires[i].Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } - DrawWire(spriteBatch, wires[i], position, wirePosition, equippedWire, panel, label); + if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } + DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label); wirePosition.Y += wireInterval; } @@ -248,18 +248,17 @@ namespace Barotrauma.Items.Components if (!PlayerInput.PrimaryMouseButtonHeld()) { if ((GameMain.NetworkMember != null || panel.CheckCharacterSuccess(Character.Controlled)) && - Wires.Count(w => w != null) < MaxPlayerConnectableWires) + Wires.Count < MaxPlayerConnectableWires) { //find an empty cell for the new connection - int index = FindEmptyIndex(); - if (index > -1 && !Wires.Contains(DraggingConnected)) + if (WireSlotsAvailable() && !Wires.Contains(DraggingConnected)) { bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); DraggingConnected.RemoveConnection(panel.Item); if (DraggingConnected.Connect(this, !alreadyConnected, true)) { var otherConnection = DraggingConnected.OtherConnection(this); - SetWire(index, DraggingConnected); + ConnectWire(DraggingConnected); } } } @@ -284,7 +283,7 @@ namespace Barotrauma.Items.Components flashColor * (float)Math.Sin(FlashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f), scale: connectorSpriteScale); } - if (Wires.Any(w => w != null && w != DraggingConnected && !w.Hidden && (!w.HiddenInGame || Screen.Selected != GameMain.GameScreen))) + if (Wires.Any(w => w != DraggingConnected && !w.Hidden && (!w.HiddenInGame || Screen.Selected != GameMain.GameScreen))) { int screwIndex = (int)Math.Floor(position.Y / 30.0f) % screwSprites.Count; screwSprites[screwIndex].Draw(spriteBatch, position, scale: connectorSpriteScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 0d026f53b..541c54a63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (item.Submarine == null || item.Submarine.Loading || Screen.Selected != GameMain.SubEditorScreen) { return; } MoveConnectedWires(amount); @@ -147,9 +147,10 @@ namespace Barotrauma.Items.Components //because some of the wires connected to the panel may not exist yet long msgStartPos = msg.BitPosition; msg.ReadUInt16(); //user ID - foreach (Connection connection in Connections) + foreach (Connection _ in Connections) { - for (int i = 0; i < connection.MaxWires; i++) + uint wireCount = msg.ReadVariableUInt32(); + for (int i = 0; i < wireCount; i++) { msg.ReadUInt16(); } @@ -173,9 +174,8 @@ namespace Barotrauma.Items.Components private void ApplyRemoteState(IReadMessage msg) { - List prevWires = Connections.SelectMany(c => c.Wires.Where(w => w != null)).ToList(); - List newWires = new List(); - + List prevWires = Connections.SelectMany(c => c.Wires).ToList(); + ushort userID = msg.ReadUInt16(); if (userID == 0) @@ -195,7 +195,9 @@ namespace Barotrauma.Items.Components foreach (Connection connection in Connections) { - for (int i = 0; i < connection.MaxWires; i++) + HashSet newWires = new HashSet(); + uint wireCount = msg.ReadVariableUInt32(); + for (int i = 0; i < wireCount; i++) { ushort wireId = msg.ReadUInt16(); @@ -204,9 +206,18 @@ namespace Barotrauma.Items.Components if (wireComponent == null) { continue; } newWires.Add(wireComponent); + } - connection.SetWire(i, wireComponent); - wireComponent.Connect(connection, false); + Wire[] oldWires = connection.Wires.Where(w => !newWires.Contains(w)).ToArray(); + foreach (var wire in oldWires) + { + connection.DisconnectWire(wire); + } + + foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) + { + connection.ConnectWire(wire); + wire.Connect(connection, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 50a7f11d3..706105771 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -45,7 +45,7 @@ namespace Barotrauma.Items.Components }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), TextManager.Get(ciElement.Label).Fallback(ciElement.Label)); - if (!ciElement.IsIntegerInput) + if (!ciElement.IsNumberInput) { var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") { @@ -77,29 +77,71 @@ namespace Barotrauma.Items.Components } else { - int.TryParse(ciElement.Signal, out int signal); - var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), GUINumberInput.NumberType.Int) + GUINumberInput numberInput = null; + if (ciElement.NumberType == NumberType.Float) { - UserData = ciElement, - MinValueInt = ciElement.NumberInputMin, - MaxValueInt = ciElement.NumberInputMax, - IntValue = Math.Clamp(signal, ciElement.NumberInputMin, ciElement.NumberInputMax) - }; - //reset size restrictions set by the Style to make sure the elements can fit the interface - numberInput.RectTransform.MinSize = numberInput.LayoutGroup.RectTransform.MinSize = new Point(0, 0); - numberInput.RectTransform.MaxSize = numberInput.LayoutGroup.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); - numberInput.OnValueChanged += (ni) => + TryParseFloatInvariantCulture(ciElement.Signal, out float floatSignal); + TryParseFloatInvariantCulture(ciElement.NumberInputMin, out float numberInputMin); + TryParseFloatInvariantCulture(ciElement.NumberInputMax, out float numberInputMax); + TryParseFloatInvariantCulture(ciElement.NumberInputStep, out float numberInputStep); + numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), NumberType.Float) + { + UserData = ciElement, + MinValueFloat = numberInputMin, + MaxValueFloat = numberInputMax, + FloatValue = Math.Clamp(floatSignal, numberInputMin, numberInputMax), + DecimalsToDisplay = ciElement.NumberInputDecimalPlaces, + valueStep = numberInputStep, + OnValueChanged = (ni) => + { + if (GameMain.Client == null) + { + ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); + } + else + { + item.CreateClientEvent(this); + } + } + }; + } + else if (ciElement.NumberType == NumberType.Int) { - if (GameMain.Client == null) + int.TryParse(ciElement.Signal, out int intSignal); + int.TryParse(ciElement.NumberInputMin, out int numberInputMin); + int.TryParse(ciElement.NumberInputMax, out int numberInputMax); + TryParseFloatInvariantCulture(ciElement.NumberInputStep, out float numberInputStep); + numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), NumberType.Int) { - ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); - } - else - { - item.CreateClientEvent(this); - } - }; - uiElements.Add(numberInput); + UserData = ciElement, + MinValueInt = numberInputMin, + MaxValueInt = numberInputMax, + IntValue = Math.Clamp(intSignal, numberInputMin, numberInputMax), + valueStep = numberInputStep, + OnValueChanged = (ni) => + { + if (GameMain.Client == null) + { + ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); + } + else + { + item.CreateClientEvent(this); + } + } + }; + } + else + { + DebugConsole.ShowError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); + } + if (numberInput != null) + { + //reset size restrictions set by the Style to make sure the elements can fit the interface + numberInput.RectTransform.MinSize = numberInput.LayoutGroup.RectTransform.MinSize = new Point(0, 0); + numberInput.RectTransform.MaxSize = numberInput.LayoutGroup.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + uiElements.Add(numberInput); + } } } else if (ciElement.ContinuousSignal) @@ -205,7 +247,7 @@ namespace Barotrauma.Items.Components foreach (var uiElement in uiElements) { if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } - bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); + 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) { @@ -293,7 +335,7 @@ namespace Barotrauma.Items.Components } else if (uiElements[i] is GUINumberInput ni) { - if (ni.InputType == GUINumberInput.NumberType.Int) + if (ni.InputType == NumberType.Int) { int.TryParse(customInterfaceElementList[i].Signal, out int value); ni.IntValue = value; @@ -307,18 +349,28 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + if (!element.IsNumberInput) { msg.Write(((GUITextBox)uiElements[i]).Text); } else { - msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + switch (element.NumberType) + { + case NumberType.Float: + msg.Write(((GUINumberInput)uiElements[i]).FloatValue.ToString()); + break; + case NumberType.Int: + default: + msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + break; + } } } - else if (customInterfaceElementList[i].ContinuousSignal) + else if (element.ContinuousSignal) { msg.Write(((GUITickBox)uiElements[i]).Selected); } @@ -333,29 +385,38 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + string newValue = msg.ReadString(); + if (!element.IsNumberInput) { - TextChanged(customInterfaceElementList[i], msg.ReadString()); + TextChanged(element, newValue); } else { - int.TryParse(msg.ReadString(), out int value); - ValueChanged(customInterfaceElementList[i], value); + 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 { bool elementState = msg.ReadBoolean(); - if (customInterfaceElementList[i].ContinuousSignal) + if (element.ContinuousSignal) { ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(customInterfaceElementList[i], elementState); + TickBoxToggled(element, elementState); } else if (elementState) { - ButtonClicked(customInterfaceElementList[i]); + ButtonClicked(element); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 8c7ef71ea..980a6f256 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -58,6 +58,8 @@ namespace Barotrauma.Items.Components return true; } }; + + layoutGroup.Recalculate(); } // Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox @@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index fcede0408..e0b3a75f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -526,7 +526,7 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { //only used in the sub editor, hence only in the client project if (!item.IsSelected) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 42fc88545..919eae413 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -175,7 +175,7 @@ namespace Barotrauma.Items.Components }; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { widgets.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index c8eb6b435..9466e4377 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -6,8 +6,10 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components { - partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable + partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { + private GUIMessageBox autodockingVerification; + public Vector2 DrawSize { //use the extents of the item as the draw size @@ -180,5 +182,10 @@ namespace Barotrauma.Items.Components Undock(); } } + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + { + msg.Write((byte)allowOutpostAutoDocking); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 96c563ea2..b23835fec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -265,7 +265,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.Prefab.Identifier == "idcard" || item.Tags.Contains("despawncontainer")) + if (item.HasTag("identitycard") || item.HasTag("despawncontainer")) { string[] readTags = item.Tags.Split(','); string idName = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 7a16fca75..32610f313 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1,4 +1,6 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -6,13 +8,8 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; -using FarseerPhysics.Dynamics; -using FarseerPhysics.Dynamics.Contacts; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -355,7 +352,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? RotationRad : -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteBatch, @@ -379,17 +376,17 @@ namespace Barotrauma } if (color.A > 0) { - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); - fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, d); + fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d); } } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { - Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, rotationRad, Scale, activeSprite.effects, depth - 0.001f); - Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, rotationRad, Scale, activeSprite.effects, depth - 0.002f); + Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f); + Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); } foreach (var decorativeSprite in Prefab.DecorativeSprites) { @@ -397,11 +394,11 @@ namespace Barotrauma float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); bool flipX = flippedX && Prefab.CanSpriteFlipX; bool flipY = flippedY && Prefab.CanSpriteFlipY; - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? rotationRad : -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? RotationRad : -RotationRad) * Scale; if (flipX) { offset.X = -offset.X; } if (flipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, + RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -448,7 +445,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } var ca = (float)Math.Cos(-body.Rotation); @@ -469,7 +466,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, @@ -572,6 +569,18 @@ namespace Barotrauma } } + public void CheckNeedsSoundUpdate(ItemComponent ic) + { + if (ic.NeedsSoundUpdate()) + { + if (!updateableComponents.Contains(ic)) + { + updateableComponents.Add(ic); + } + isActive = true; + } + } + public void UpdateSpriteStates(float deltaTime) { if (activeContainedSprite != null) @@ -720,7 +729,7 @@ namespace Barotrauma //remove identifiers from the available container tags //(otherwise the list will include many irrelevant options, //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) - .Where(t => !ItemPrefab.Prefabs.Any(ip => ip.Identifier == t)) + .Where(t => !ItemPrefab.Prefabs.ContainsKey(t)) .ToImmutableHashSet(); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") { @@ -943,6 +952,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { if (!(userData is Identifier)) { return true; } @@ -1174,7 +1184,7 @@ namespace Barotrauma texts.Clear(); string nameText = Name; - if (Prefab.Identifier == "idcard" || Tags.Contains("despawncontainer")) + if (Prefab.Tags.Contains("identitycard") || Tags.Contains("despawncontainer")) { string[] readTags = Tags.Split(','); string idName = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 264b5b2e7..0fc5d2c4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -183,8 +183,21 @@ namespace Barotrauma.MapCreatures.Behavior Color branchColor = (branch.IsRoot || branch.IsRootGrowth) ? RootColor : Color.White; + if (GameMain.DebugDraw) { + if (branch.DisconnectedFromRoot && branch.ParentBranch == null) + { + branchColor = Color.Yellow; + } + else if (branch.DisconnectedFromRoot) + { + branchColor = Color.Cyan; + } + else if (branch.ParentBranch == null) + { + branchColor = Color.Magenta; + } #if DEBUG Vector2 basePos = Parent.WorldPosition; foreach (var (from, to) in debugSearchLines) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 57979fdd0..088ffdf3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -33,7 +33,7 @@ namespace Barotrauma DrawArrow(FlowTargetHull, IsHorizontal ? rect.Height: rect.Width, Math.Abs(lerpedFlowForce.Length()), Color.Red * 0.3f); } - if (outsideCollisionBlocker.Enabled && Submarine != null) + if (Submarine != null && outsideCollisionBlocker != null && outsideCollisionBlocker.Enabled) { var edgeShape = outsideCollisionBlocker.FixtureList[0].Shape as FarseerPhysics.Collision.Shapes.EdgeShape; Vector2 startPos = ConvertUnits.ToDisplayUnits(outsideCollisionBlocker.GetWorldPoint(edgeShape.Vertex1)) + Submarine.Position; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index ed2f608d8..797801633 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -80,15 +80,17 @@ namespace Barotrauma } Vector2 center = new Vector2((minX + maxX) / 2.0f, (minY + maxY) / 2.0f); if (Submarine.MainSub != null) { center -= Submarine.MainSub.HiddenSubPosition; } - center.X -= MathUtils.RoundTowardsClosest(center.X, Submarine.GridSize.X); - center.Y -= MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y); + + Vector2 offsetFromGrid = new Vector2( + MathUtils.RoundTowardsClosest(center.X, Submarine.GridSize.X) - center.X, + MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { - mapEntity.Move(-center); + mapEntity.Move(-center - offsetFromGrid); mapEntity.Submarine = Submarine.MainSub; var entityElement = mapEntity.Save(element); if (disabledEntities.Contains(mapEntity)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 475553ddf..f92c7a30f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -60,12 +60,6 @@ namespace Barotrauma } public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { IsDisposed = true; WallEdgeBuffer?.Dispose(); @@ -482,12 +476,6 @@ namespace Barotrauma } public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { foreach (var vertexBuffer in vertexBuffers) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index ea9eebd9c..a053bcc73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -230,14 +230,6 @@ namespace Barotrauma public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing) return; - if (WaterEffect != null) { WaterEffect.Dispose(); @@ -250,6 +242,5 @@ namespace Barotrauma basicEffect = null; } } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 0483137ee..27b1aaf28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -461,7 +461,7 @@ namespace Barotrauma.Lights Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); - SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix); + SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); } private void CalculateDimensions() @@ -541,7 +541,7 @@ namespace Barotrauma.Lights } } - public void SetVertices(Vector2[] points, Matrix? rotationMatrix = null) + public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); @@ -594,13 +594,16 @@ namespace Barotrauma.Lights if (ParentEntity == null) { return; } - var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); - if (chList != null) + if (mergeOverlappingSegments) { - overlappingHulls.Clear(); - foreach (ConvexHull ch in chList.List) + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); + if (chList != null) { - MergeOverlappingSegments(ch); + overlappingHulls.Clear(); + foreach (ConvexHull ch in chList.List) + { + MergeOverlappingSegments(ch); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index d1764fd10..e49b2cf55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -11,6 +11,18 @@ namespace Barotrauma.Lights { class LightManager { + /// + /// How many light sources are allowed to recalculate their light volumes per frame. + /// Pending calculations will be done on subsequent frames, starting from the light sources that have been waiting for a recalculation the longest. + /// + const int MaxLightVolumeRecalculationsPerFrame = 5; + + /// + /// If zoomed further out than this, characters no longer obstruct lights behind them. + /// Improves performance, and isn't very noticeable if we do it after zoomed far out enough. + /// + const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; + public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -59,6 +71,8 @@ namespace Barotrauma.Lights private Vector2 losOffset; + private int recalculationCount; + public IEnumerable Lights { get { return lights; } @@ -151,6 +165,9 @@ namespace Barotrauma.Lights } private readonly List activeLights = new List(capacity: 100); + private readonly List activeLightsWithLightVolume = new List(capacity: 100); + + public static int ActiveLightCount { get; private set; } public void Update(float deltaTime) { @@ -180,11 +197,13 @@ namespace Barotrauma.Lights Rectangle viewRect = cam.WorldView; viewRect.Y -= cam.WorldView.Height; //check which lights need to be drawn + recalculationCount = 0; activeLights.Clear(); foreach (LightSource light in lights) { if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } + if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); @@ -205,8 +224,44 @@ namespace Barotrauma.Lights range = Math.Max(Math.Max(spriteRange, targetSize), range); } if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, range, viewRect)) { continue; } - activeLights.Add(light); + + light.Priority = lightPriority(range, light); + + int i = 0; + while (i < activeLights.Count && light.Priority < activeLights[i].Priority) + { + i++; + } + activeLights.Insert(i, light); } + ActiveLightCount = activeLights.Count; + + float lightPriority(float range, LightSource light) + { + return + 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)); + } + + //find the lights with an active light volume + activeLightsWithLightVolume.Clear(); + foreach (var activeLight in activeLights) + { + if (activeLight.Range < 1.0f || activeLight.Color.A < 1 || activeLight.CurrentBrightness <= 0.0f) { continue; } + activeLightsWithLightVolume.Add(activeLight); + } + + //remove some lights with a light volume if there's too many of them + if (activeLightsWithLightVolume.Count > GameSettings.CurrentConfig.Graphics.VisibleLightLimit) + { + for (int i = GameSettings.CurrentConfig.Graphics.VisibleLightLimit; i < activeLightsWithLightVolume.Count; i++) + { + activeLights.Remove(activeLightsWithLightVolume[i]); + } + } + activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); //draw light sprites attached to characters //render into a separate rendertarget using alpha blending (instead of on top of everything else with alpha blending) @@ -235,7 +290,7 @@ namespace Barotrauma.Lights { if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } light.DrawSprite(spriteBatch, cam); - light.DrawLightVolume(spriteBatch, lightEffect, transform); + light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -243,14 +298,6 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - /*if (backgroundObstructor != null) - { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); - spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, - (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); - spriteBatch.End(); - }*/ - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); Dictionary visibleHulls = GetVisibleHulls(cam); foreach (KeyValuePair hull in visibleHulls) @@ -292,41 +339,44 @@ namespace Barotrauma.Lights //draw characters to obstruct the highlighted items/characters and light sprites //--------------------------------------------------------------------------------------------------- - - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); - foreach (Character character in Character.CharacterList) + if (cam.Zoom > ObstructLightsBehindCharactersZoomThreshold) { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } - if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : - character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); - foreach (Limb limb in character.AnimController.Limbs) + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); + foreach (Character character in Character.CharacterList) { - if (limb.DeformSprite != null) { continue; } - limb.Draw(spriteBatch, cam, lightColor); + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.DeformSprite != null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); + } } - } - spriteBatch.End(); + spriteBatch.End(); - DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; - DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); - foreach (Character character in Character.CharacterList) - { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } - if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : - character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); - foreach (Limb limb in character.AnimController.Limbs) + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); + foreach (Character character in Character.CharacterList) { - if (limb.DeformSprite == null) { continue; } - limb.Draw(spriteBatch, cam, lightColor); + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.DeformSprite == null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); + } } + spriteBatch.End(); } - spriteBatch.End(); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; graphics.BlendState = BlendState.Additive; @@ -344,7 +394,7 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } - light.DrawLightVolume(spriteBatch, lightEffect, transform); + light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); } lightEffect.World = transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 0eacc74f3..5a4454ccf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Lights private VertexPositionColorTexture[] vertices; private short[] indices; - private List hullsInRange; + private readonly List hullsInRange; public Texture2D texture; @@ -246,9 +246,9 @@ namespace Barotrauma.Lights } //when were the vertices of the light volume last calculated - private float lastRecalculationTime; + public float LastRecalculationTime { get; private set; } - private Dictionary diffToSub; + private readonly Dictionary diffToSub; private DynamicVertexBuffer lightVolumeBuffer; private DynamicIndexBuffer lightVolumeIndexBuffer; @@ -376,6 +376,8 @@ namespace Barotrauma.Lights } } + public float Priority; + private Vector2 lightTextureTargetSize; public Vector2 LightTextureTargetSize @@ -423,7 +425,7 @@ namespace Barotrauma.Lights public bool Enabled = true; - private ISerializableEntity conditionalTarget; + private readonly ISerializableEntity conditionalTarget; private readonly PropertyConditional.Comparison comparison; private readonly List conditionals = new List(); @@ -561,7 +563,7 @@ namespace Barotrauma.Lights foreach (var ch in chList.List) { - if (ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch)) + if (ch.LastVertexChangeTime > LastRecalculationTime && !chList.IsHidden.Contains(ch)) { NeedsRecalculation = true; break; @@ -1289,7 +1291,7 @@ namespace Barotrauma.Lights } //visualize light recalculations - float timeSinceRecalculation = (float)Timing.TotalTime - lastRecalculationTime; + float timeSinceRecalculation = (float)Timing.TotalTime - LastRecalculationTime; if (timeSinceRecalculation < 0.1f) { GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); @@ -1313,7 +1315,7 @@ namespace Barotrauma.Lights } } - public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform) + public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount) { if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } @@ -1338,8 +1340,9 @@ namespace Barotrauma.Lights CheckHullsInRange(); - if (NeedsRecalculation) + if (NeedsRecalculation && allowRecalculation) { + recalculationCount++; var verts = FindRaycastHits(); if (verts == null) { @@ -1352,7 +1355,7 @@ namespace Barotrauma.Lights CalculateLightVertices(verts); - lastRecalculationTime = (float)Timing.TotalTime; + LastRecalculationTime = (float)Timing.TotalTime; NeedsRecalculation = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 4f0dfbe2c..ecdbeeacc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -98,7 +98,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(); + Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); InitProjectSpecific(); return true; } @@ -642,11 +642,11 @@ namespace Barotrauma } } - if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.HasOutpost())) + if (GameMain.DebugDraw) { - if (location.Reputation != null) + Vector2 dPos = pos; + if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) { - Vector2 dPos = pos; dPos.Y += 48; string name = $"Reputation: {location.Name}"; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -663,6 +663,8 @@ namespace Barotrauma GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } + dPos.Y += 48; + GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatZeroDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); } } } @@ -977,7 +979,7 @@ namespace Barotrauma Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) { - GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); + GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + (int)connection.Difficulty + ")", Color.White); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 5f4645cac..365db83ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -81,11 +81,18 @@ namespace Barotrauma } catch (System.IO.FileNotFoundException e) { - string errorMsg = "Failed to load sound file \"" + filename + "\"."; + string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; DebugConsole.ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } + catch (System.IO.InvalidDataException e) + { + string errorMsg = "Failed to load sound file \"" + filename + "\" (invalid data)."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:InvalidData" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } } RoundSound newSound = new RoundSound(element, existingSound); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index eff30e678..44c3c5e92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -1,55 +1,61 @@ -using Barotrauma.Networking; -using Barotrauma.RuinGeneration; -using Barotrauma.Sounds; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma { partial class Submarine : Entity, IServerPositionSync { - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) - { - Vector2 position = PlayerInput.MousePosition; - position = cam.ScreenToWorld(position); - - Vector2 worldGridPos = VectorToWorldGrid(position); - - if (sub != null) - { - worldGridPos.X += sub.Position.X % GridSize.X; - worldGridPos.Y += sub.Position.Y % GridSize.Y; - } - - return worldGridPos; - } - //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); + + private static double prevCullTime; + private static Rectangle prevCullArea; + /// + /// Interval at which we force culled entites to be updated, regardless if the camera has moved + /// + private const float CullInterval = 0.25f; + /// + /// Margin applied around the view area when culling entities (i.e. entities that are this far outside the view are still considered visible) + /// + private const int CullMargin = 500; + /// + /// Update entity culling when any corner of the view has moved more than this + /// + private const int CullMoveThreshold = 50; + public static void CullEntities(Camera cam) { + Rectangle camView = cam.WorldView; + camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2); + + if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold && + Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold && + Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold && + Math.Abs(camView.Bottom - prevCullArea.Bottom) < CullMoveThreshold && + prevCullTime > Timing.TotalTime - CullInterval) + { + return; + } + visibleSubs.Clear(); foreach (Submarine sub in Loaded) { if (Level.Loaded != null && sub.WorldPosition.Y < Level.MaxEntityDepth) { continue; } - int margin = 500; Rectangle worldBorders = new Rectangle( - sub.VisibleBorders.X + (int)sub.WorldPosition.X - margin, - sub.VisibleBorders.Y + (int)sub.WorldPosition.Y + margin, - sub.VisibleBorders.Width + margin * 2, - sub.VisibleBorders.Height + margin * 2); + sub.VisibleBorders.X + (int)sub.WorldPosition.X, + sub.VisibleBorders.Y + (int)sub.WorldPosition.Y, + sub.VisibleBorders.Width, + sub.VisibleBorders.Height); - if (RectsOverlap(worldBorders, cam.WorldView)) + if (RectsOverlap(worldBorders, camView)) { visibleSubs.Add(sub); } @@ -64,16 +70,22 @@ namespace Barotrauma visibleEntities.Clear(); } - Rectangle worldView = cam.WorldView; foreach (MapEntity entity in MapEntity.mapEntityList) { if (entity.Submarine != null) { if (!visibleSubs.Contains(entity.Submarine)) { continue; } } - - if (entity.IsVisible(worldView)) { visibleEntities.Add(entity); } + if (entity.IsVisible(camView)) { visibleEntities.Add(entity); } } + + prevCullArea = camView; + prevCullTime = Timing.TotalTime; + } + + public static void ForceVisibilityRecheck() + { + prevCullTime = 0; } public static void Draw(SpriteBatch spriteBatch, bool editing = false) @@ -148,7 +160,7 @@ namespace Barotrauma { if (predicate != null) { - if (!predicate(e)) continue; + if (!predicate(e)) { continue; } } float drawDepth = structure.GetDrawDepth(); int i = 0; @@ -525,7 +537,7 @@ namespace Barotrauma Item.ItemList.Count(it2 => it2.linkedTo.Contains(item) && !item.linkedTo.Contains(it2)); for (int i = 0; i < item.Connections.Count; i++) { - int wireCount = item.Connections[i].Wires.Count(w => w != null); + int wireCount = item.Connections[i].Wires.Count; if (doorLinks + wireCount > item.Connections[i].MaxWires) { errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", @@ -679,6 +691,22 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) + { + Vector2 position = PlayerInput.MousePosition; + position = cam.ScreenToWorld(position); + + Vector2 worldGridPos = VectorToWorldGrid(position); + + if (sub != null) + { + worldGridPos.X += sub.Position.X % GridSize.X; + worldGridPos.Y += sub.Position.Y % GridSize.Y; + } + + return worldGridPos; + } + public void ClientReadPosition(IReadMessage msg, float sendingTime) { var posInfo = PhysicsBody.ClientRead(msg, sendingTime, parentDebugName: Info.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index c8e5cedeb..463d94253 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -8,7 +8,7 @@ namespace Barotrauma partial class SubmarineInfo : IDisposable { public Sprite PreviewImage; - + partial void InitProjectSpecific() { string previewImageData = SubmarineElement.GetAttributeString("previewimage", ""); @@ -154,13 +154,13 @@ namespace Barotrauma crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); } - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + if (RecommendedCrewExperience != CrewExperienceLevel.Unknown) { var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + TextManager.Get(RecommendedCrewExperience.ToIdentifier()), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 5aa7c9197..e72fc7fb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -16,7 +16,7 @@ namespace Barotrauma class SubmarinePreview : IDisposable { private SpriteRecorder spriteRecorder; - private SubmarineInfo submarineInfo; + private readonly SubmarineInfo submarineInfo; private Camera camera; private Task loadTask; private volatile bool isDisposed; @@ -66,7 +66,7 @@ namespace Barotrauma public static void Close() { - instance?.Dispose(); + instance?.Dispose(); instance = null; } private SubmarinePreview(SubmarineInfo subInfo) @@ -100,12 +100,16 @@ namespace Barotrauma GUIListBox specsContainer = null; new GUICustomComponent(new RectTransform(Vector2.One, innerPadded.RectTransform, Anchor.Center), - (spriteBatch, component) => { + (spriteBatch, component) => + { + if (isDisposed) { return; } camera.UpdateTransform(interpolate: true, updateListener: false); Rectangle drawRect = new Rectangle(component.Rect.X + 1, component.Rect.Y + 1, component.Rect.Width - 2, component.Rect.Height - 2); RenderSubmarine(spriteBatch, drawRect, component); }, - (deltaTime, component) => { + (deltaTime, component) => + { + if (isDisposed) { return; } bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && @@ -294,8 +298,8 @@ namespace Barotrauma private void BakeMapEntity(XElement element) { - string identifier = element.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(identifier)) { return; } + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { return; } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Equals(Rectangle.Empty)) { return; } @@ -308,7 +312,16 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + MapEntityPrefab prefab = null; + if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && + ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + { + prefab = ip; + } + else + { + prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + } if (prefab == null) { return; } var texture = prefab.Sprite.Texture; @@ -329,7 +342,6 @@ namespace Barotrauma bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; - StructurePrefab structurePrefab = prefab as StructurePrefab; if (itemPrefab != null) { BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); @@ -337,7 +349,7 @@ namespace Barotrauma if (!overrideSprite) { - if (structurePrefab != null) + if (prefab is StructurePrefab structurePrefab) { ParseUpgrades(structurePrefab.ConfigElement, ref scale); @@ -655,7 +667,8 @@ namespace Barotrauma previewFrame.RectTransform.Parent = null; previewFrame = null; } - spriteRecorder?.Dispose(); + spriteRecorder?.Dispose(); spriteRecorder = null; + camera?.Dispose(); camera = null; isDisposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 9e0472592..a020b263a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System; namespace Barotrauma.Networking @@ -9,14 +10,15 @@ namespace Barotrauma.Networking { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)Type); + msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); msg.Write(Text); } public static void ClientRead(IReadMessage msg) { UInt16 id = msg.ReadUInt16(); - ChatMessageType type = (ChatMessageType)msg.ReadByte(); + ChatMessageType type = (ChatMessageType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; string styleSetting = string.Empty; @@ -183,6 +185,11 @@ namespace Barotrauma.Networking break; default: GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor); + if (type == ChatMessageType.Radio && CanUseRadio(senderCharacter, out WifiComponent radio)) + { + Signal s = new Signal(txt, sender: senderCharacter, source: radio.Item); + radio.TransmitSignal(s, sentFromChat: true); + } break; } LastID = id; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 75401668a..1a0efc16c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -146,26 +146,19 @@ namespace Barotrauma.Networking } private bool disposed = false; - protected virtual void Dispose(bool disposing) - { - if (disposed) return; - - if (disposing) - { - if (WriteStream != null) - { - WriteStream.Flush(); - WriteStream.Close(); - WriteStream.Dispose(); - WriteStream = null; - } - } - disposed = true; - } - + public void Dispose() { - Dispose(true); + if (disposed) { return; } + + if (WriteStream != null) + { + WriteStream.Flush(); + WriteStream.Close(); + WriteStream.Dispose(); + WriteStream = null; + } + disposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index ce27b16ae..fdb7451f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3,6 +3,7 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.IO.Compression; using System.Linq; @@ -11,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework.Input; namespace Barotrauma.Networking { @@ -182,6 +184,20 @@ namespace Barotrauma.Networking get { return ownerKey > 0 || steamP2POwner; } } + internal readonly struct PermissionChangedEvent + { + public readonly ClientPermissions NewPermissions; + public readonly ImmutableArray NewPermittedConsoleCommands; + + public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) + { + NewPermissions = newPermissions; + NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray(); + } + } + + public readonly NamedEvent OnPermissionChanged = new NamedEvent(); + public GameClient(string newName, string ip, UInt64 steamId, string serverName = null, int ownerKey = 0, bool steamP2POwner = false) { //TODO: gui stuff should probably not be here? @@ -570,7 +586,12 @@ namespace Barotrauma.Networking public override void Update(float deltaTime) { #if DEBUG - if (PlayerInput.GetKeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.P)) return; + if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.P)) return; + + if (PlayerInput.KeyHit(Keys.Home)) + { + OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, permittedConsoleCommands)); + } #endif foreach (Client c in ConnectedClients) @@ -668,7 +689,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text.Text == ChildServerRelay.CrashMessage)) + if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) { var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; @@ -803,7 +824,11 @@ namespace Barotrauma.Networking byte campaignID = inc.ReadByte(); UInt16 campaignSaveID = inc.ReadUInt16(); - UInt16 campaignUpdateID = inc.ReadUInt16(); + Dictionary campaignUpdateIDs = new Dictionary(); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaignUpdateIDs[flag] = inc.ReadUInt16(); + } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -822,7 +847,7 @@ namespace Barotrauma.Networking campaign != null && campaign.CampaignID == campaignID && campaign.LastSaveID == campaignSaveID && - campaign.LastUpdateID == campaignUpdateID; + campaignUpdateIDs.All(kvp => campaign.GetLastUpdateIdForFlag(kvp.Key) == kvp.Value); } readyToStartMsg.Write(readyToStart); @@ -1019,40 +1044,25 @@ namespace Barotrauma.Networking GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers); } - byte equalityCheckValueCount = inc.ReadByte(); - List levelEqualityCheckValues = new List(); - for (int i = 0; i < equalityCheckValueCount; i++) + var levelEqualityCheckValues = new Dictionary(); + foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType().OrderBy(s => s)) { - levelEqualityCheckValues.Add(inc.ReadInt32()); + levelEqualityCheckValues.Add(stage, inc.ReadInt32()); } - if (Level.Loaded.EqualityCheckValues.Count != levelEqualityCheckValues.Count) + foreach (var stage in levelEqualityCheckValues.Keys) { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + - " (client value count: " + Level.Loaded.EqualityCheckValues.Count + - ", level value count: " + levelEqualityCheckValues.Count + - ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new Exception(errorMsg); - } - else - { - for (int i = 0; i < equalityCheckValueCount; i++) + if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage]) { - if (Level.Loaded.EqualityCheckValues[i] != levelEqualityCheckValues[i]) - { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + - " (client value #" + i + ": " + Level.Loaded.EqualityCheckValues[i] + - ", server value #" + i + ": " + levelEqualityCheckValues[i].ToString("X") + - ", level value count: " + levelEqualityCheckValues.Count + - ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new Exception(errorMsg); - } + string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + + " (client value " + stage + ": " + Level.Loaded.EqualityCheckValues[stage].ToString("X") + + ", server value " + stage + ": " + levelEqualityCheckValues[stage].ToString("X") + + ", level value count: " + levelEqualityCheckValues.Count + + ", seed: " + Level.Loaded.Seed + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + + ", mirrored: " + Level.Loaded.Mirrored + ")."; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); } } @@ -1076,6 +1086,7 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = null; + GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); GUI.ClearCursorWait(); @@ -1189,7 +1200,14 @@ namespace Barotrauma.Networking new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; connected = false; + + var prevContentPackages = clientPeer.ServerContentPackages; ConnectToServer(serverEndpoint, serverName); + if (clientPeer != null) + { + //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match + clientPeer.ServerContentPackages = prevContentPackages; + } } else { @@ -1380,18 +1398,13 @@ namespace Barotrauma.Networking private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) { if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || - permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) + permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) { if (newPermissions == permissions) return; } - bool refreshCampaignUI = false; - - if (permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || - permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound)) - { - refreshCampaignUI = true; - } + bool refreshCampaignUI = permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || + permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound); permissions = newPermissions; this.permittedConsoleCommands = new List(permittedConsoleCommands); @@ -1430,7 +1443,7 @@ namespace Barotrauma.Networking if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands)) { var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), - TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); + TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)); foreach (string permittedCommand in permittedConsoleCommands) { @@ -1469,6 +1482,7 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.RefreshEnabledElements(); + OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, this.permittedConsoleCommands)); } private IEnumerable StartGame(IReadMessage inc) @@ -1516,6 +1530,7 @@ namespace Barotrauma.Networking serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); + serverSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); @@ -1681,6 +1696,13 @@ namespace Barotrauma.Networking } } + if (clientPeer == null) + { + DebugConsole.ThrowError("There was an error initializing the round (disconnected during the StartGame coroutine.)"); + roundInitStatus = RoundInitStatus.Error; + yield return CoroutineStatus.Failure; + } + roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; DateTime? timeOut = null; @@ -1823,7 +1845,8 @@ namespace Barotrauma.Networking GameMain.GameScreen.Select(); - AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); + // TODO: Re-enable the server message once it's been edited and translated + //AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); yield return CoroutineStatus.Success; } @@ -2397,7 +2420,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(netFlag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2442,7 +2468,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(flag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2489,6 +2518,7 @@ namespace Barotrauma.Networking message, type, gameStarted && myCharacter != null ? myCharacter : null); + chatMessage.ChatMode = GameMain.ActiveChatMode; lastQueueChatMsgID++; chatMessage.NetStateID = lastQueueChatMsgID; @@ -2640,7 +2670,7 @@ namespace Barotrauma.Networking if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -2670,9 +2700,12 @@ namespace Barotrauma.Networking } DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID); - //decrement campaign update ID so the server will send us the latest data + //decrement campaign update IDs so the server will send us the latest data //(as there may have been campaign updates after the save file was created) - campaign.LastUpdateID--; + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1)); + } break; case FileTransferType.Mod: if (!(Screen.Selected is ModDownloadScreen)) { return; } @@ -2771,10 +2804,21 @@ namespace Barotrauma.Networking GameMain.GameSession = null; } - public void WriteCharacterInfo(IWriteMessage msg) + public void SendCharacterInfo(string newName = null) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); + WriteCharacterInfo(msg, newName); + msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + clientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { msg.Write(characterInfo == null); - if (characterInfo == null) return; + if (characterInfo == null) { return; } + + msg.Write(newName ?? string.Empty); msg.Write((byte)characterInfo.Head.Preset.TagSet.Count); foreach (Identifier tag in characterInfo.Head.Preset.TagSet) @@ -2820,18 +2864,18 @@ namespace Barotrauma.Networking } #region Submarine Change Voting - public void InitiateSubmarineChange(SubmarineInfo sub, VoteType voteType) + public void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType) { if (sub == null) { return; } - Vote(voteType, sub); + Vote(voteType, (sub, transferItems)); } - public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, float timeOut) + public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut) { if (info == null) { return; } if (votingInterface != null && votingInterface.VoteRunning) { return; } votingInterface?.Remove(); - votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, timeOut); + votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, transferItems, timeOut); } #endregion @@ -3010,7 +3054,7 @@ namespace Barotrauma.Networking msg.Write(mapSeed); msg.Write(sub.Name); msg.Write(sub.MD5Hash.StringRepresentation); - settings.Serialize(msg); + msg.Write(settings); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3258,10 +3302,8 @@ namespace Barotrauma.Networking { if (GUI.KeyboardDispatcher.Subscriber == null) { - bool chatKeyHit = PlayerInput.KeyHit(InputType.Chat); - bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100); - - if (chatKeyHit || radioKeyHit) + var chatKeyStates = ChatBox.ChatKeyStates.GetChatKeyStates(); + if (chatKeyStates.AnyHit) { if (msgBox.Selected) { @@ -3272,34 +3314,8 @@ namespace Barotrauma.Networking { if (Screen.Selected == GameMain.GameScreen) { - if (chatKeyHit) - { - msgBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.DarkGreen, 0.5f); - if (!chatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - } - - if (radioKeyHit) - { - msgBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); - if (!chatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - - if (!msgBox.Text.StartsWith(ChatBox.RadioChatString)) - { - msgBox.Text = ChatBox.RadioChatString; - } - } + ChatBox.ApplySelectionInputs(msgBox, false, chatKeyStates); } - msgBox.Select(msgBox.Text.Length); } } @@ -3567,13 +3583,13 @@ namespace Barotrauma.Networking return true; }; - durationInputDays = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), GUINumberInput.NumberType.Int) + durationInputDays = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueFloat = 1000 }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get("Days")); - durationInputHours = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), GUINumberInput.NumberType.Int) + durationInputHours = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueFloat = 24 @@ -3680,7 +3696,9 @@ namespace Barotrauma.Networking } if (Level.Loaded != null) { - errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); + errorLines.Add("Level: " + Level.Loaded.Seed + ", " + + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv + => cv.Key + "=" + cv.Value.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index e26337d66..050355919 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -126,7 +126,7 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - var numInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), GUINumberInput.NumberType.Int) + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) { MinValueInt = min, MaxValueInt = max diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 82ce2130f..673a8423f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -6,7 +8,8 @@ { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)ChatMessageType.Order); + msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); WriteOrder(msg); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 3134ee4f8..f39baaaec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -50,7 +50,7 @@ namespace Barotrauma.Networking } } - public ImmutableArray ServerContentPackages { get; private set; } = + public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; public delegate void MessageCallback(IReadMessage message); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 47201cef6..ed3738964 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -117,8 +117,6 @@ namespace Barotrauma.Networking PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); - //Console.WriteLine(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte); - if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) { ReadConnectionInitializationStep(new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 2b72f5dfa..6e097aabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Networking timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - + PacketHeader packetHeader = (PacketHeader)data[0]; if (!packetHeader.IsServerMessage()) { return; } @@ -373,12 +373,6 @@ namespace Barotrauma.Networking OnDisconnect?.Invoke(disableReconnect); } - ~SteamP2PClientPeer() - { - OnDisconnect = null; - Close(); - } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) { Steamworks.P2PSend sendType; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 5bd83236c..2f5c2f480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -455,12 +455,6 @@ namespace Barotrauma.Networking ChildServerRelay.Write(bufToSend); } - ~SteamP2POwnerPeer() - { - OnDisconnect = null; - Close(); - } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) { //not currently used by SteamP2POwnerPeer diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 430df186b..f03d7b099 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Networking else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } + if (numInput.InputType == NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } } return null; } @@ -56,7 +56,7 @@ namespace Barotrauma.Networking else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); else if (GUIComponent is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { numInput.IntValue = (int)value; } @@ -480,15 +480,12 @@ namespace Barotrauma.Networking // game settings //-------------------------------------------------------------------------------- - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; + var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) { }; + GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsTab.RectTransform)); // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); - var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), roundsTab.RectTransform)) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); + var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) { AutoHideScrollBar = true, UseGridLayout = true @@ -510,11 +507,16 @@ namespace Barotrauma.Networking GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsTab.RectTransform)) + { + Stretch = true + }; + + var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), TextManager.Get("ServerSettingsEndRoundVoting")); GetPropertyData(nameof(AllowEndVoting)).AssignGUIComponent(endVoteBox); - CreateLabeledSlider(roundsTab, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); + CreateLabeledSlider(sliderLayout, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); LocalizedString endRoundLabel = sliderLabel.Text; slider.Step = 0.2f; @@ -527,11 +529,11 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")); GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); - CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); + CreateLabeledSlider(sliderLayout, "ServerSettingsRespawnInterval", out slider, out sliderLabel); LocalizedString intervalLabel = sliderLabel.Text; slider.Range = new Vector2(10.0f, 600.0f); slider.StepValue = 10.0f; @@ -544,7 +546,7 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), roundsTab.RectTransform), + var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), sliderLayout.RectTransform), isHorizontal: true); var minRespawnLayout @@ -611,17 +613,18 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsTab.RectTransform)); - var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), TextManager.Get("LosEffect")); var losModeRadioButtonLayout - = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), losModeLayout.RectTransform), isHorizontal: true) { Stretch = true }; - + var losModeRadioButtonGroup = new GUIRadioButtonGroup(); LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); for (int i = 0; i < losModes.Length; i++) @@ -631,16 +634,29 @@ namespace Barotrauma.Networking } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - var traitorsMinPlayerCount = CreateLabeledNumberInput(roundsTab, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); + GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsTab.RectTransform)) + { + Stretch = true + }; + + var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); - var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); + var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); + GetPropertyData(nameof(MaximumMoneyTransferRequest)).AssignGUIComponent(maximumTransferAmount); + + var lootedMoneyDestination = CreateLabeledDropdown(numberLayout, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); + GetPropertyData(nameof(LootedMoneyDestination)).AssignGUIComponent(lootedMoneyDestination); + + var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - var buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), roundsTab.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f @@ -747,7 +763,7 @@ namespace Barotrauma.Networking ExtraCargo.TryGetValue(ip, out int cargoVal); var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), - GUINumberInput.NumberType.Int, textAlignment: Alignment.CenterLeft) + NumberType.Int, textAlignment: Alignment.CenterLeft) { MinValueInt = 0, MaxValueInt = MaxExtraCargoItemsOfType, @@ -979,7 +995,7 @@ namespace Barotrauma.Networking { label.ToolTip = TextManager.Get(toolTipTag); } - var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), GUINumberInput.NumberType.Int) + var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) { MinValueInt = min, MaxValueInt = max @@ -991,6 +1007,32 @@ namespace Barotrauma.Networking return input; } + private GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get(labelTag) + }; + + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) + { + AutoScaleHorizontal = true + }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } + var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); + + container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); + + return input; + } + private bool SelectSettingsTab(GUIButton button, object obj) { settingsTabIndex = (int)obj; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cf2caabed..f1c611704 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -227,20 +227,10 @@ namespace Barotrauma.Networking bool allowEnqueue = overrideSound != null; if (GameMain.WindowActive && SettingsMenu.Instance is null) { - ForceLocal = captureTimer > 0 ? ForceLocal : GameSettings.CurrentConfig.Audio.UseLocalVoiceByDefault; - bool pttDown = false; - if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && - GUI.KeyboardDispatcher.Subscriber == null) + bool pttDown = PlayerInput.KeyDown(InputType.Voice) && GUI.KeyboardDispatcher.Subscriber == null; + if (pttDown || captureTimer <= 0) { - pttDown = true; - if (PlayerInput.KeyDown(InputType.LocalVoice)) - { - ForceLocal = true; - } - else - { - ForceLocal = false; - } + ForceLocal = GameMain.ActiveChatMode == ChatMode.Local; } if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { @@ -257,7 +247,6 @@ namespace Barotrauma.Networking } } } - if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index de7227cc8..a889a9b4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -7,6 +7,20 @@ namespace Barotrauma { partial class Voting { + private struct SubmarineVoteInfo + { + public SubmarineInfo SubmarineInfo { get; set; } + public bool TransferItems { get; set; } + public int DeliveryFee { get; set; } + + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + { + SubmarineInfo = submarineInfo; + TransferItems = transferItems; + DeliveryFee = deliveryFee; + } + } + private readonly Dictionary voteCountYes = new Dictionary(), voteCountNo = new Dictionary(), @@ -131,14 +145,16 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is SubmarineInfo voteSub) + if (data is (SubmarineInfo voteSub, bool transferItems)) { //initiate sub vote msg.Write(true); msg.Write(voteSub.Name); + msg.Write(transferItems); } else { + // vote if (!(data is int)) { return; } msg.Write(false); msg.Write((int)data); @@ -246,7 +262,7 @@ namespace Barotrauma float timeOut = inc.ReadByte(); Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); - if (!myClient.InGame) { return; } + if (myClient == null || !myClient.InGame) { return; } switch (voteType) { @@ -254,13 +270,14 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName1 = inc.ReadString(); + bool transferItems = inc.ReadBoolean(); SubmarineInfo info = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName1); if (info == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } - GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, timeOut); + GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, transferItems, timeOut); break; case VoteType.TransferMoney: byte fromClientId = inc.ReadByte(); @@ -279,39 +296,40 @@ namespace Barotrauma case VoteState.Passed: case VoteState.Failed: bool passed = inc.ReadBoolean(); - - SubmarineInfo subInfo = null; + SubmarineVoteInfo submarineVoteInfo = default; switch (voteType) { case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - subInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); - if (subInfo == null) + var submarineInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + bool transferItems = inc.ReadBoolean(); + int deliveryFee = inc.ReadInt16(); + if (submarineInfo == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - if (passed && subInfo != null) + if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { - int deliveryFee = inc.ReadInt16(); switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, deliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index fa77bcd3e..8af7d4e14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -103,7 +103,7 @@ namespace Barotrauma.Particles { return debugName; } - public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { this.prefab = prefab; #if DEBUG @@ -149,13 +149,13 @@ namespace Barotrauma.Particles if (prefab.LifeTimeMin <= 0.0f) { - totalLifeTime = prefab.LifeTime; - lifeTime = prefab.LifeTime; + totalLifeTime = prefab.LifeTime * lifeTimeMultiplier; + lifeTime = prefab.LifeTime * lifeTimeMultiplier; } else { - totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime); - lifeTime = totalLifeTime; + totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime) * lifeTimeMultiplier; + lifeTime = totalLifeTime * lifeTimeMultiplier; } startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f0457e030..cae3906e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -85,6 +85,9 @@ namespace Barotrauma.Particles [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } + [Editable, Serialize(1f, IsPropertySaveable.Yes)] + public float LifeTimeMultiplier { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DrawOnTop { get; set; } @@ -197,7 +200,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 558faee69..982106567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } @@ -115,7 +115,7 @@ namespace Barotrauma.Particles if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index c7cd94394..5e679768b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,8 +1,11 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; +using System.Collections.Immutable; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -15,11 +18,11 @@ namespace Barotrauma protected GUITextBox saveNameBox, seedBox; protected GUIButton loadGameButton; - + public Action StartNewGame; public Action LoadGame; - protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 } protected CategoryFilter subFilter = CategoryFilter.All; public GUIButton StartButton @@ -33,15 +36,11 @@ namespace Barotrauma get; protected set; } - - public GUITickBox EnableRadiationToggle { get; set; } - public GUILayoutGroup CampaignSettingsContent { get; set; } + public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } - public GUITextBlock MaxMissionCountText; - public CampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) { this.newGameContainer = newGameContainer; @@ -102,5 +101,264 @@ namespace Barotrauma return saveFrame; } + + public struct CampaignSettingElements + { + public SettingValue RadiationEnabled; + public SettingValue MaxMissionCount; + public SettingValue StartingFunds; + public SettingValue Difficulty; + public SettingValue StartItemSet; + + public readonly CampaignSettings CreateSettings() + { + return new CampaignSettings(element: null) + { + RadiationEnabled = RadiationEnabled.GetValue(), + MaxMissionCount = MaxMissionCount.GetValue(), + StartingBalanceAmount = StartingFunds.GetValue(), + Difficulty = Difficulty.GetValue(), + StartItemSet = StartItemSet.GetValue() + }; + } + } + + public readonly struct SettingValue + { + private readonly Func getter; + private readonly Action setter; + + public T GetValue() + { + return getter.Invoke(); + } + + public void SetValue(T value) + { + setter.Invoke(value); + } + + public SettingValue(Func get, Action set) + { + getter = get; + setter = set; + } + } + + private readonly struct SettingCarouselElement + { + public readonly LocalizedString Label; + public readonly T Value; + public readonly bool IsHidden; + + public SettingCarouselElement(T value, string label, bool isHidden = false) + { + Value = value; + Label = TextManager.Get(label).Fallback(label); + IsHidden = isHidden; + } + } + + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + { + const float verticalSize = 0.14f; + + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length); + + presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); + + foreach (CampaignSettings settings in CampaignModePresets.List) + { + string name = settings.PresetName; + presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + } + + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) + { + Spacing = GUI.IntScale(5) + }; + + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + + ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); + SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); + + ImmutableArray> fundOptions = ImmutableArray.Create( + new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high"), + new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), + new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low") + ); + + SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions); + + ImmutableArray> difficultyOptions = ImmutableArray.Create( + new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), + new SettingCarouselElement(GameDifficulty.Medium, "difficulty.medium"), + new SettingCarouselElement(GameDifficulty.Hard, "difficulty.hard"), + new SettingCarouselElement(GameDifficulty.Hellish, "difficulty.hellish", isHidden: true) + ); + + SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; + SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); + + SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), + prevSettings.MaxMissionCount, + valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, + verticalSize); + + presetDropdown.OnSelected = (selected, o) => + { + if (o is CampaignSettings settings) + { + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + return true; + } + return false; + }; + + return new CampaignSettingElements + { + RadiationEnabled = radiationEnabled, + MaxMissionCount = maxMissionCountInput, + StartingFunds = startingFundsInput, + Difficulty = difficultyInput, + StartItemSet = startingSetInput + }; + + // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + { + ClickSound = GUISoundType.Decrease, + UserData = -valueStep + }; + GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputContainer.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", + hidePlusMinusButtons: true) + { + IntValue = defaultValue, + MinValueInt = minValue, + MaxValueInt = maxValue + }; + inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + { + ClickSound = GUISoundType.Increase, + UserData = valueStep + }; + + minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + numberInput.IntValue += change; + return true; + } + + return new SettingValue(() => numberInput.IntValue, i => numberInput.IntValue = i); + } + + static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, + ImmutableArray> options) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1 }; + GUIFrame inputFrame = new GUIFrame(new RectTransform(Vector2.One, inputContainer.RectTransform), style: null); + GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", hidePlusMinusButtons: true) + { + IntValue = options.IndexOf(defaultValue), + MinValueInt = 0, + MaxValueInt = options.Length, + Visible = false + }; + inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + GUITextBox inputLabel = new GUITextBox(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), text: defaultValue.Label.Value, textAlignment: Alignment.Center, createPenIcon: false) + { + CanBeFocused = false + }; + + GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1 }; + + minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + int hiddenOptions = 0; + + for (int i = options.Length - 1; i >= 0; i--) + { + if (options[i].IsHidden) + { + hiddenOptions++; + continue; + } + break; + } + + int limit = options.Length - hiddenOptions; + + if (PlayerInput.IsShiftDown()) + { + limit = options.Length; + } + + int newValue = MathUtils.PositiveModulo(Math.Clamp(numberInput.IntValue + change, min: -1, max: limit), limit); + SetValue(newValue); + return true; + } + + void SetValue(int value) + { + numberInput.IntValue = value; + inputLabel.Text = options[value].Label.Value; + } + + return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); + } + + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); + GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); + GUITickBox tickBox = new GUITickBox(new RectTransform(Vector2.One, tickboxContainer.RectTransform), string.Empty) + { + Selected = defaultValue, + ToolTip = tooltip + }; + tickBox.Box.IgnoreLayoutGroups = true; + tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); + inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); + } + + static GUILayoutGroup CreateSettingBase(GUIComponent parent, LocalizedString description, LocalizedString tooltip, float horizontalSize, float verticalSize) + { + GUILayoutGroup settingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), description, font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, wrap: true) { ToolTip = tooltip }; + GUILayoutGroup inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + inputContainer.RectTransform.IsFixedSize = true; + settingHolder.RectTransform.MinSize = new Point(0, (int)descriptionBlock.TextSize.Y); + return inputContainer; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 97e08e561..0c297bbf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -18,71 +18,35 @@ namespace Barotrauma var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.0f + RelativeSpacing = 0.05f + }; + + GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true + }; + + GUILayoutGroup campaignSettingLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.05f }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { - textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } + textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - GUIFrame radiationBoxContainer - = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), style: null); - GUITickBox radiationEnabledTickBox = null; - if (MapGenerationParams.Instance.RadiationParams != null) - { - radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = true, - OnSelected = box => true - }; - } + nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true) - { - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - int maxMissionCount = GameMain.NetworkMember.ServerSettings.MaxMissionCount; - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleLeft"); - var maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); - void updateMissionCountText() - { - maxMissionCount = MathHelper.Clamp(maxMissionCount, - CampaignSettings.MinMissionCountLimit, - CampaignSettings.MaxMissionCountLimit); - - maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); - } - maxMissionCountButtons[1] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleRight"); - maxMissionCountButtons[0].OnClicked = (button, o) => - { - maxMissionCount--; - updateMissionCountText(); - return false; - }; - maxMissionCountButtons[1].OnClicked = (button, o) => - { - maxMissionCount++; - updateMissionCountText(); - return false; - }; - updateMissionCountText(); - maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.04f), + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) @@ -99,7 +63,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } selectedSub = GameMain.NetLobbyScreen.SelectedSub; - + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); @@ -115,11 +79,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings - { - RadiationEnabled = radiationEnabledTickBox?.Selected ?? GameMain.NetworkMember.ServerSettings.RadiationEnabled, - MaxMissionCount = maxMissionCount - }; + CampaignSettings settings = elements.CreateSettings(); if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -172,12 +132,16 @@ namespace Barotrauma }; StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = 8000; + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); + } if (GameMain.NetLobbyScreen.SelectedSub != null) { initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; @@ -238,6 +202,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -257,7 +222,7 @@ namespace Barotrauma file1WriteTime = File.GetLastWriteTime(file1); } catch - { + { //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list }; try diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 870827d91..b093b494d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -1,12 +1,11 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using System.Globalization; -using Barotrauma.Extensions; namespace Barotrauma { @@ -142,7 +141,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUIStyle.SubHeadingFont); var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); @@ -155,8 +154,12 @@ namespace Barotrauma { Stretch = true }; - - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) + { + PlaySoundOnSelect = true, + ScrollBarVisible = true + }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); @@ -191,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -200,12 +203,25 @@ namespace Barotrauma return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(); + CreateCustomizeWindow(CurrentSettings, settings => + { + CampaignSettings prevSettings = CurrentSettings; + CurrentSettings = settings; + if (prevSettings.InitialMoney != settings.InitialMoney) + { + object selectedData = subList.SelectedData; + UpdateSubList(SubmarineInfo.SavedSubmarines); + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + { + subList.Select(selectedData); + } + } + }); return true; } }; @@ -218,7 +234,7 @@ namespace Barotrauma return false; } }; - + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { IgnoreLayoutGroups = true, @@ -353,54 +369,21 @@ namespace Barotrauma StealRandomizeButton(CharacterMenus[i], jobTextContainer); } } - - private void CreateCustomizeWindow() + + private void CreateCustomizeWindow(CampaignSettings prevSettings, Action onClosed = null) { - CampaignCustomizeSettings = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); - CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; + CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); - CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) - { - RelativeSpacing = 0.1f - }; + GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - if (MapGenerationParams.Instance.RadiationParams != null) - { - bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = prevRadiationToggleEnabled, - ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") - }; - } - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true); - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } - }; - RichString prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); - MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); - maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } + + onClosed?.Invoke(elements.CreateSettings()); + return CampaignCustomizeSettings.Close(button, o); }; - maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); } private static void StealRandomizeButton(CharacterInfo.AppearanceCustomizationMenu menu, GUIComponent parent) @@ -412,7 +395,7 @@ namespace Barotrauma randomizeButton.RectTransform.Parent = parent.RectTransform; randomizeButton.RectTransform.RelativeSize = Vector2.One * 1.3f; } - + private bool FinishSetup(GUIButton btn, object userdata) { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) @@ -420,7 +403,7 @@ namespace Barotrauma saveNameBox.Flash(GUIStyle.Red); return false; } - + SubmarineInfo selectedSub = null; if (!(subList.SelectedData is SubmarineInfo)) { return false; } @@ -443,16 +426,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings(); - settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text.SanitizedValue, out int missionCount)) - { - settings.MaxMissionCount = missionCount; - } - else - { - settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; - } + CampaignSettings settings = CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -499,7 +473,7 @@ namespace Barotrauma return true; } - + public void RandomizeSeed() { seedBox.Text = ToolBox.RandomSeed(8); @@ -509,7 +483,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - var sub = child.UserData as SubmarineInfo; + SubmarineInfo sub = child.UserData as SubmarineInfo; if (sub == null) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } @@ -523,7 +497,7 @@ namespace Barotrauma if (!(obj is SubmarineInfo sub)) { return true; } #if !DEBUG - if (sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -554,10 +528,10 @@ namespace Barotrauma subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); } - subsToShow.Sort((s1, s2) => + subsToShow.Sort((s1, s2) => { - int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; - int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; + int p1 = s1.Price > CurrentSettings.InitialMoney ? 10 : 0; + int p2 = s2.Price > CurrentSettings.InitialMoney ? 10 : 0; return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); @@ -572,7 +546,7 @@ namespace Barotrauma ToolTip = sub.Description, UserData = sub }; - + if (!sub.RequiredContentPackagesInstalled) { textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); @@ -582,13 +556,13 @@ namespace Barotrauma var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -598,7 +572,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); @@ -625,6 +599,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -650,8 +625,9 @@ namespace Barotrauma { var saveFrame = CreateSaveElement(saveInfo); if (saveFrame == null) { continue; } - + XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); + if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); @@ -725,9 +701,10 @@ namespace Barotrauma string subName = doc.Root.GetAttributeString("submarine", ""); string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); + DateTime? time = null; if (long.TryParse(saveTime, out long unixTime)) { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + time = ToolBox.Epoch.ToDateTime(unixTime); saveTime = time.ToString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 81973be7f..3d0a30141 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -526,7 +526,7 @@ namespace Barotrauma tickBox.OnSelected += (GUITickBox tb) => { if (!Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - + if (tb.Selected) { Campaign.Map.CurrentLocation.SelectMission(mission); @@ -549,7 +549,7 @@ namespace Barotrauma { GameMain.Client?.SendCampaignState(); } - return true; + return true; }; missionTickBoxes.Add(tickBox); @@ -729,7 +729,15 @@ namespace Barotrauma break; case CampaignMode.InteractionType.PurchaseSub: if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); - submarineSelection.RefreshSubmarineDisplay(true); + submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); + break; + case CampaignMode.InteractionType.Map: + //refresh mission rewards (may have been changed by e.g. a pending submarine switch) + foreach (GUITextBlock rewardText in missionRewardTexts) + { + Mission mission = (Mission)rewardText.UserData; + rewardText.Text = mission.GetMissionRewardText(Submarine.MainSub); + } break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index e9de25b54..11105e73a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2280,7 +2280,7 @@ namespace Barotrauma.CharacterEditor var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + NumberType.Int, relativeButtonAreaWidth: 0.25f) { Font = GUIStyle.SmallFont }; @@ -2858,7 +2858,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; void PopulateListBox() @@ -2996,7 +2999,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; // Type filtering diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 6027aa90a..2f8f73962 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -482,7 +482,10 @@ namespace Barotrauma.CharacterEditor RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUIStyle.SubHeadingFont); - var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { OnClicked = (b, d) => @@ -523,7 +526,7 @@ namespace Barotrauma.CharacterEditor { var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) + GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -659,7 +662,10 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { OnClicked = (b, d) => @@ -863,7 +869,7 @@ namespace Barotrauma.CharacterEditor var limbTypeField = GUI.CreateEnumField(limbType, elementSize, GetCharacterEditorTranslation("LimbType"), group.RectTransform, font: GUIStyle.Font); var sourceRectField = GUI.CreateRectangleField(sourceRect ?? new Rectangle(0, 100 * LimbGUIElements.Count, 100, 100), elementSize, GetCharacterEditorTranslation("SourceRectangle"), group.RectTransform, font: GUIStyle.Font); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("ID")); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, @@ -912,7 +918,7 @@ namespace Barotrauma.CharacterEditor }; var limb1Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")); - var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, @@ -920,7 +926,7 @@ namespace Barotrauma.CharacterEditor }; var limb2Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")); - var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 9e690bb7b..38202f624 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -38,9 +38,9 @@ namespace Barotrauma } // attach number inputs to our generated parent elements - var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.R }; - var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.G }; - var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.B }; + var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), NumberType.Int) { IntValue = BackgroundColor.R }; + var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), NumberType.Int) { IntValue = BackgroundColor.G }; + var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), NumberType.Int) { IntValue = BackgroundColor.B }; rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 2e450a813..22b86c510 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -225,7 +225,7 @@ namespace Barotrauma return true; } - public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm, GUISoundType? overrideConfirmButtonSound = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); @@ -244,6 +244,10 @@ namespace Barotrauma msgBox.Close(); return true; }; + if (overrideConfirmButtonSound.HasValue) + { + msgBox.Buttons[0].ClickSound = overrideConfirmButtonSound.Value; + } return msgBox; } @@ -818,7 +822,7 @@ namespace Barotrauma } else if (type == typeof(float) || type == typeof(int)) { - GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; } else if (type == typeof(bool)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 4efe53c5c..f3783a78a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -126,7 +126,7 @@ namespace Barotrauma DrawMap(graphics, spriteBatch, deltaTime); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("DrawMap", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map", sw.ElapsedTicks); sw.Restart(); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); @@ -165,7 +165,7 @@ namespace Barotrauma spriteBatch.End(); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("DrawHUD", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:HUD", sw.ElapsedTicks); sw.Restart(); } @@ -178,6 +178,9 @@ namespace Barotrauma GameMain.ParticleManager.UpdateTransforms(); + Stopwatch sw = new Stopwatch(); + sw.Start(); + GameMain.LightManager.ObstructVision = Character.Controlled != null && Character.Controlled.ObstructVision && @@ -185,6 +188,10 @@ namespace Barotrauma GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled?.CursorWorldPosition ?? Vector2.Zero); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:LOS", sw.ElapsedTicks); + sw.Restart(); + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); graphics.Clear(Color.Transparent); @@ -196,9 +203,17 @@ namespace Barotrauma Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackStructures", sw.ElapsedTicks); + sw.Restart(); + graphics.SetRenderTarget(null); GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam, renderTarget); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:Lighting", sw.ElapsedTicks); + sw.Restart(); + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTargetBackground); if (Level.Loaded == null) @@ -228,6 +243,10 @@ namespace Barotrauma spriteBatch.Draw(renderTarget, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackLevel", sw.ElapsedTicks); + sw.Restart(); + //---------------------------------------------------------------------------- //Start drawing to the normal render target (stuff that can't be seen through the LOS effect) @@ -248,6 +267,10 @@ namespace Barotrauma } spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackCharactersItems", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); DrawDeformed(firstPass: true); DrawDeformed(firstPass: false); @@ -266,8 +289,16 @@ namespace Barotrauma } } + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:DeformableCharacters", sw.ElapsedTicks); + sw.Restart(); + Level.Loaded?.DrawFront(spriteBatch, cam); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontLevel", sw.ElapsedTicks); + sw.Restart(); + //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater graphics.SetRenderTarget(renderTargetWater); @@ -302,6 +333,10 @@ namespace Barotrauma WaterRenderer.Instance.RenderAir(graphics, cam, renderTarget, Cam.ShaderTransform); graphics.DepthStencilState = DepthStencilState.None; + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, @@ -310,10 +345,18 @@ namespace Barotrauma Submarine.DrawDamageable(spriteBatch, damageEffect, false); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontDamageable", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); Submarine.DrawFront(spriteBatch, false, null); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontStructuresItems", sw.ElapsedTicks); + sw.Restart(); + //draw additive particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, DepthStencilState.Default, null, null, cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.Additive); @@ -349,6 +392,10 @@ namespace Barotrauma } spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontMisc", sw.ElapsedTicks); + sw.Restart(); + if (GameMain.LightManager.LosEnabled && GameMain.LightManager.LosMode != LosMode.None && Lights.LightManager.ViewTarget != null) { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; @@ -457,6 +504,10 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Lerp(Color.TransparentBlack, Color.Black, fadeToBlackState), isFilled: true); spriteBatch.End(); } + + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); + sw.Restart(); } partial void UpdateProjSpecific(double deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 146f41f13..3d026e56a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -34,12 +34,16 @@ namespace Barotrauma private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; + private readonly GUIDropDown selectedSubDropDown; + private Sprite editingSprite; private LightSource pointerLightSource; private readonly Color[] tunnelDebugColors = new Color[] { Color.White, Color.Cyan, Color.LightGreen, Color.Red, Color.LightYellow, Color.LightSeaGreen }; + private LevelData currentLevelData; + public LevelEditorScreen() { Cam = new Camera() @@ -55,19 +59,26 @@ namespace Barotrauma RelativeSpacing = 0.01f }; - paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)); + paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; paramsList.OnSelected += (GUIComponent component, object obj) => { selectedParams = obj as LevelGenerationParams; + currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); editorContainer.ClearChildren(); - SortLevelObjectsList(selectedParams); + SortLevelObjectsList(currentLevelData); new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, false, true, elementHeight: 20); return true; }; var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUIStyle.SubHeadingFont); - ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; ruinParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -76,7 +87,10 @@ namespace Barotrauma var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUIStyle.SubHeadingFont); - caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; caveParamsList.OnSelected += (GUIComponent component, object obj) => { CreateCaveParamsEditor(obj as CaveGenerationParams); @@ -86,7 +100,10 @@ namespace Barotrauma var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); - outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); + outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; outpostParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -149,7 +166,7 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if(seedBox == null) { return false; } + if (seedBox == null) { return false; } seedBox.Text = GetLevelSeed(); return true; } @@ -168,6 +185,16 @@ namespace Barotrauma Vector2 GetSeedElementRelativeSize() => new Vector2(0.5f * (1.0f - randomizeButtonRelativeSize.X), 1.0f); static string GetLevelSeed() => ToolBox.RandomSeed(8); + var subDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform), TextManager.Get("submarine")); + selectedSubDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform)); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + selectedSubDropDown.AddItem(sub.DisplayName, userData: sub); + } + subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.RectTransform.MinSize.Y); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), @@ -183,11 +210,18 @@ namespace Barotrauma { bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); + + if (selectedSubDropDown.SelectedData is SubmarineInfo subInfo) + { + Submarine.MainSub = new Submarine(subInfo); + } GameMain.LightManager.ClearLights(); - LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); - levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; - levelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - Level.Generate(levelData, mirror: mirrorLevel.Selected); + currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); + currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; + var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); + Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { @@ -225,7 +259,7 @@ namespace Barotrauma var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() .Where(f => !(f is SubmarineFile))).ToArray(); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == GameSettings.CurrentConfig.QuickStartSub); + SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); @@ -256,6 +290,7 @@ namespace Barotrauma levelObjectList = new GUIListBox(new RectTransform(new Vector2(0.99f, 0.85f), bottomPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, UseGridLayout = true }; levelObjectList.OnSelected += (GUIComponent component, object obj) => @@ -417,11 +452,11 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier.Value), textAlignment: Alignment.Center); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = caveGenerationParams.GetCommonness(selectedParams, abyss: false), + FloatValue = caveGenerationParams.GetCommonness(currentLevelData, abyss: false), OnValueChanged = (numberInput) => { caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; @@ -478,18 +513,18 @@ namespace Barotrauma var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUIStyle.SubHeadingFont); outpostParamsEditor.AddCustomContent(moduleLabel, 100); - foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) + foreach (var moduleCount in outpostGenerationParams.ModuleCounts) { var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key.Value), textAlignment: Alignment.CenterLeft); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Identifier.Value), textAlignment: Alignment.CenterLeft); + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 100, - IntValue = moduleCount.Value, + IntValue = moduleCount.Count, OnValueChanged = (numInput) => { - outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); + outpostGenerationParams.SetModuleCount(moduleCount.Identifier, numInput.IntValue); if (numInput.IntValue == 0) { outpostParamsList.Select(outpostParamsList.SelectedData); @@ -505,7 +540,7 @@ namespace Barotrauma var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); HashSet availableFlags = new HashSet(); - foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } @@ -516,7 +551,7 @@ namespace Barotrauma text: TextManager.Get("leveleditor.addmoduletype")); foreach (Identifier flag in availableFlags) { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key == flag)) { continue; } + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Identifier == flag)) { continue; } moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); } moduleTypeDropDown.OnSelected += (_, userdata) => @@ -542,7 +577,7 @@ namespace Barotrauma if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } foreach (var caveParam in CaveGenerationParams.CaveParams) { - if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } + if (selectedParams != null && caveParam.GetCommonness(currentLevelData, abyss: false) <= 0.0f) { continue; } availableIdentifiers.Add(caveParam.Identifier); } availableIdentifiers.Reverse(); @@ -557,11 +592,11 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId.Value), textAlignment: Alignment.Center); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(selectedParams) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), + FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(currentLevelData) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), OnValueChanged = (numberInput) => { levelObjectPrefab.OverrideCommonness[paramsId] = numberInput.FloatValue; @@ -620,7 +655,7 @@ namespace Barotrauma childObj.AllowedNames = dropdown.SelectedDataMultiple.Select(d => ((LevelObjectPrefab)d).Name).ToList(); return true; }; - new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, @@ -630,7 +665,7 @@ namespace Barotrauma selectedChildObj.MaxCount = Math.Max(selectedChildObj.MaxCount, selectedChildObj.MinCount); } }.IntValue = childObj.MinCount; - new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, @@ -689,13 +724,13 @@ namespace Barotrauma buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.Children.First().MinSize; } - private void SortLevelObjectsList(LevelGenerationParams selectedParams) + private void SortLevelObjectsList(LevelData levelData) { //fade out levelobjects that don't spawn in this type of level foreach (GUIComponent levelObjFrame in levelObjectList.Content.Children) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; - float commonness = levelObj.GetCommonness(selectedParams); + float commonness = levelObj.GetCommonness(levelData); levelObjFrame.Color = commonness > 0.0f ? GUIStyle.Green * 0.4f : Color.Transparent; levelObjFrame.SelectedColor = commonness > 0.0f ? GUIStyle.Green * 0.6f : Color.White * 0.5f; levelObjFrame.HoverColor = commonness > 0.0f ? GUIStyle.Green * 0.7f : Color.White * 0.6f; @@ -712,7 +747,7 @@ namespace Barotrauma { var levelObj1 = c1.GUIComponent.UserData as LevelObjectPrefab; var levelObj2 = c2.GUIComponent.UserData as LevelObjectPrefab; - return Math.Sign(levelObj2.GetCommonness(selectedParams) - levelObj1.GetCommonness(selectedParams)); + return Math.Sign(levelObj2.GetCommonness(levelData) - levelObj1.GetCommonness(levelData)); }); } @@ -863,7 +898,11 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - item?.GetComponent()?.Update((float)deltaTime, Cam); + if (item == null) { continue; } + foreach (var light in item.GetComponents()) + { + light.Update((float)deltaTime, Cam); + } } } GameMain.LightManager?.Update((float)deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 9334f7060..88691410e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -74,12 +74,24 @@ namespace Barotrauma { CreateHostServerFields(); CreateCampaignSetupUI(); + SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); - foreach (var subElement in remoteContentDoc.Root.Elements()) + try { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + foreach (var subElement in remoteContentDoc.Root.Elements()) + { + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + } + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentParse:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received remote main menu content failed. " + e.Message); } } }; @@ -418,7 +430,10 @@ namespace Barotrauma //PLACEHOLDER var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); + new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) + { + PlaySoundOnSelect = true, + }; var tutorialTypes = new List() { typeof(MechanicTutorial), @@ -1229,7 +1244,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Decrease }; maxPlayersBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), textAlignment: Alignment.Center) { @@ -1249,7 +1265,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Increase }; maxPlayersLabel.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 0656aacea..537124f37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -20,10 +20,11 @@ namespace Barotrauma private ServerContentPackage? currentDownload; private readonly List downloadedPackages = new List(); + public IEnumerable DownloadedPackages => downloadedPackages; private bool confirmDownload; - private void Reset() + public void Reset() { pendingDownloads.Clear(); downloadedPackages.Clear(); @@ -255,12 +256,6 @@ namespace Barotrauma } } - public override void Deselect() - { - Reset(); - base.Deselect(); - } - public override void Update(double deltaTime) { base.Update(deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 2e9797dba..4bf99f6d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -110,7 +110,7 @@ namespace Barotrauma public bool CampaignCharacterDiscarded { get; - private set; + set; } //elements that can only be used by the host @@ -179,7 +179,7 @@ namespace Barotrauma get { return ModeList.SelectedIndex; } set { - ModeList.Select(value, true); + ModeList.Select(value, GUIListBox.Force.Yes); } } @@ -504,6 +504,7 @@ namespace Barotrauma PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } }; @@ -816,6 +817,7 @@ namespace Barotrauma SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -901,6 +903,7 @@ namespace Barotrauma }; ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -1250,9 +1253,6 @@ namespace Barotrauma CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - - /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } - jobPreferenceSprites.Clear();*/ } public override void Select() @@ -1400,7 +1400,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, parent: parent, createPendingText: createPendingText); @@ -1414,7 +1414,7 @@ namespace Barotrauma characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); characterInfo.RecreateHead(MultiplayerPreferences.Instance); GameMain.Client.CharacterInfo = characterInfo; - characterInfo.OmitJobInPortraitClothing = false; + characterInfo.OmitJobInMenus = true; } parent.ClearChildren(); @@ -1515,6 +1515,7 @@ namespace Barotrauma JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, + PlaySoundOnSelect = true, OnSelected = (child, obj) => { if (child.IsParentOf(GUI.MouseOn)) return false; @@ -1600,6 +1601,7 @@ namespace Barotrauma { Enabled = true, KeepSpaceForScrollBar = false, + PlaySoundOnSelect = true, ScrollBarEnabled = false, ScrollBarVisible = false }; @@ -2902,6 +2904,7 @@ namespace Barotrauma appearanceFrame.ClearChildren(); var info = GameMain.Client.CharacterInfo ?? Character.Controlled?.Info; + CharacterAppearanceCustomizationMenu?.Dispose(); CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { OnHeadSwitch = menu => @@ -3131,10 +3134,11 @@ namespace Barotrauma retVal[i] = new GUIImage[outfitPreview.Sprites.Count]; for (int j = 0; j < outfitPreview.Sprites.Count; j++) { - Pair sprite = outfitPreview.Sprites[j]; + Sprite sprite = outfitPreview.Sprites[j].sprite; + Vector2 drawOffset = outfitPreview.Sprites[j].drawOffset; float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X; retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) - { RelativeOffset = sprite.Second / outfitPreview.Dimensions }, sprite.First, scaleToFit: true) + { RelativeOffset = drawOffset / outfitPreview.Dimensions }, sprite, scaleToFit: true) { PressedColor = Color.White, CanBeFocused = false @@ -3183,7 +3187,7 @@ namespace Barotrauma var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; - if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, true); } + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, GUIListBox.Force.Yes); } selectedModeIndex = modeIndex; if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) @@ -3299,7 +3303,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - ModeList.Select(GameModePreset.MultiPlayerCampaign, true); + ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } } @@ -3415,7 +3419,7 @@ namespace Barotrauma UserData = i, OnClicked = (btn, obj) => { - JobList.Select((int)obj, true); + JobList.Select((int)obj, GUIListBox.Force.Yes); SwitchJob(btn, null); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } JobList.Deselect(); @@ -3551,7 +3555,7 @@ namespace Barotrauma else { subList.OnSelected -= VotableClicked; - subList.Select(sub, force: true); + subList.Select(sub, GUIListBox.Force.Yes); subList.OnSelected += VotableClicked; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 81473f6d6..c48aa0ac5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -129,7 +129,10 @@ namespace Barotrauma OnClicked = (btn, userdata) => { FilterEmitters(""); filterBox.Text = ""; filterBox.Flash(Color.White); return true; } }; - prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)); + prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true, + }; prefabList.OnSelected += (GUIComponent component, object obj) => { cam.Position = Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 1bf91b69e..49bed19f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -504,6 +504,7 @@ namespace Barotrauma serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (btn, obj) => { @@ -1473,7 +1474,6 @@ namespace Barotrauma { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { - Font = GUIStyle.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index aad8f3493..df8fc28ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -85,12 +85,12 @@ namespace Barotrauma { OnClicked = (button, userData) => { - var selected = selectedSprites; + var selected = selectedSprites.ToList(); Sprite firstSelected = selected.First(); selected.ForEach(s => s.ReloadTexture()); RefreshLists(); - textureList.Select(firstSelected.FullPath, autoScroll: false); - selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); + textureList.Select(firstSelected.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); + selected.ForEachMod(s => spriteList.Select(s, autoScroll: GUIListBox.AutoScroll.Disabled)); texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); texturePathText.TextColor = GUIStyle.Green; return true; @@ -206,6 +206,7 @@ namespace Barotrauma textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { var newTexturePath = userData as string; @@ -213,7 +214,7 @@ namespace Barotrauma { selectedTexturePath = newTexturePath; ResetZoom(); - spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: false); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); } foreach (GUIComponent child in spriteList.Content.Children) @@ -248,6 +249,7 @@ namespace Barotrauma spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { if (userData is Sprite sprite) @@ -291,7 +293,7 @@ namespace Barotrauma }, style: null, color: Color.Black * 0.6f); var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -481,7 +483,7 @@ namespace Barotrauma var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); if (scaledRect.Contains(PlayerInput.MousePosition)) { - spriteList.Select(sprite, autoScroll: false); + spriteList.Select(sprite, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); UpdateScrollBar(textureList); // Release the keyboard so that we can nudge the source rects @@ -847,7 +849,7 @@ namespace Barotrauma base.Select(); LoadSprites(); RefreshLists(); - spriteList.Select(0, autoScroll: false); + spriteList.Select(0, autoScroll: GUIListBox.AutoScroll.Disabled); } protected override void DeselectEditorSpecific() @@ -905,7 +907,7 @@ namespace Barotrauma } if (sprite.FullPath != selectedTexturePath) { - textureList.Select(sprite.FullPath, autoScroll: false); + textureList.Select(sprite.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(textureList); } xmlPathText.Text = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 73e46e383..61a57a7ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; using Barotrauma.IO; +using Barotrauma.Steam; namespace Barotrauma { @@ -58,15 +59,6 @@ namespace Barotrauma islinked = Linkage; } } - - #warning TODO: switch this to an enum? - private static readonly ImmutableArray crewExperienceLevels = new string[] - { - "CrewExperienceLow", - "CrewExperienceMid", - "CrewExperienceHigh" - }.ToImmutableArray(); - public enum Mode { @@ -99,6 +91,8 @@ namespace Barotrauma private SubmarineInfo backedUpSubInfo; + private readonly HashSet publishedWorkshopItemIds = new HashSet(); + private Point screenResolution; private bool lightingEnabled; @@ -550,6 +544,7 @@ namespace Barotrauma }; previouslyUsedList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), previouslyUsedPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = SelectPrefab }; @@ -635,6 +630,7 @@ namespace Barotrauma undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (_, userData) => { @@ -1006,7 +1002,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - Visible = false + Visible = false, + PlaySoundOnSelect = true, }; paddedTab.Recalculate(); @@ -1136,7 +1133,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - ClampMouseRectToParent = true + ClampMouseRectToParent = true, + PlaySoundOnSelect = true, }; entityListInner.ContentBackground.ClampMouseRectToParent = true; entityListInner.Content.ClampMouseRectToParent = true; @@ -1190,18 +1188,23 @@ namespace Barotrauma frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = ep.Description.IsNullOrEmpty() ? name : name + '\n' + ep.Description; + frame.ToolTip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + if (!ep.Description.IsNullOrEmpty()) + { + frame.ToolTip += '\n' + ep.Description; + } if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) { frame.Color = Color.Magenta; - frame.ToolTip = RichString.Rich($"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"); + frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } if (ep.HideInMenus) { frame.Color = Color.Red; name = "[HIDDEN] " + name; } + frame.ToolTip = RichString.Rich(frame.ToolTip); GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -1321,6 +1324,17 @@ namespace Barotrauma { base.Select(); + TaskPool.Add( + $"DeterminePublishedItemIds", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + + publishedWorkshopItemIds.Clear(); + publishedWorkshopItemIds.UnionWith(items.Select(it => it.Id.Value)); + }); + GUI.PreventPauseMenuToggle = false; if (!Directory.Exists(autoSavePath)) { @@ -1535,6 +1549,8 @@ namespace Barotrauma MapEntity.DeselectAll(); ClearUndoBuffer(); + DebugConsole.DeactivateCheats(); + SetMode(Mode.Default); SoundPlayer.OverrideMusicType = Identifier.Empty; @@ -1568,15 +1584,11 @@ namespace Barotrauma ClearFilter(); ClearLayers(); - while (packageReloadQueue.TryDequeue(out var p)) - { - ContentPackageManager.ReloadContentPackage(p); - } } private void CreateDummyCharacter() { - if (dummyCharacter != null) RemoveDummyCharacter(); + if (dummyCharacter != null) { RemoveDummyCharacter(); } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter.Info.Name = "Galldren"; @@ -1694,8 +1706,20 @@ namespace Barotrauma autoSaveLabel?.FadeOut(0.5f, true, 1f); } - private bool SaveSub(GUIButton button, object obj) + private bool SaveSub(ContentPackage packageToSaveTo) { + void handleExceptions(Action action) + { + try + { + action(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while trying to save {nameBox.Text}", e, createMessageBox: true); + } + } + if (string.IsNullOrWhiteSpace(nameBox.Text)) { GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); @@ -1717,7 +1741,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { - SaveSubToFile(nameBox.Text); + handleExceptions(() => SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; msgBox.Close(); return true; @@ -1730,17 +1754,22 @@ namespace Barotrauma return true; } - var result = SaveSubToFile(nameBox.Text); + bool result = false; + handleExceptions(() => result = SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; return result; } - private readonly Queue packageReloadQueue = new Queue(); - - private void EnqueueForReload(ContentPackage p) + private void ReloadModifiedPackage(ContentPackage p) { if (p is null) { return; } - if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } + p.ReloadSubsAndItemAssemblies(); + if (p.Files.Length == 0) + { + Directory.Delete(p.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } } public static Type DetermineSubFileType(SubmarineType type) @@ -1755,12 +1784,9 @@ namespace Barotrauma SubmarineType.Player => typeof(SubmarineFile), _ => null }; - - private bool SaveSubToFile(string name) - { - bool canModifyPackage(ContentPackage p) - => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + private bool SaveSubToFile(string name, ContentPackage packageToSaveTo) + { Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) @@ -1779,6 +1805,7 @@ namespace Barotrauma modProject.AddFile(newFile); } + using var _ = Validation.SkipInDebugBuilds(); modProject.DiscardHashAndInstallTime(); modProject.Save(packagePath); } @@ -1814,61 +1841,61 @@ namespace Barotrauma name = name.Trim(); string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}"; - - var vanilla = GameMain.VanillaContent; - var vanillaSubs = vanilla?.GetFiles()?.Select(f => f.Path); - bool isVanillaSub = vanillaSubs?.Any(f => f.Value == MainSub.Info.FilePath.CleanUpPath()) ?? false; - string savePath = name + ".sub"; + string savePath = $"{name}.sub"; string prevSavePath = null; - if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && - MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + if (packageToSaveTo != null) + { + var modProject = new ModProject(packageToSaveTo); + var fileListPath = packageToSaveTo.Path; + if (packageToSaveTo == ContentPackageManager.VanillaCorePackage) + { +#if !DEBUG + throw new InvalidOperationException("Cannot save to Vanilla package"); +#endif + savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch + { + SubmarineType.Player => "Content/Submarines/{0}", + SubmarineType.Outpost => "Content/Map/Outposts/{0}", + SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... + SubmarineType.Wreck => "Content/Map/Wrecks/{0}", + SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", + SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", + SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", + _ => throw new InvalidOperationException() + }, savePath); + modProject.ModVersion = ""; + } + else + { + savePath = Path.Combine(packageToSaveTo.Dir, savePath); + } + addSubAndSaveModProject(modProject, savePath, fileListPath); + } + else if (MainSub?.Info?.FilePath != null + && MainSub.Info.Name != null + && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) + && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = MainSub.Info.FilePath.CleanUpPath(); - string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); - string[] subDirs = prevDir.Split('/'); - - ModProject modProject = new ModProject() { Name = name }; - string fileListPath = null; - - if (subDirs.Length > 1 && subDirs[0].Equals(ContentPackage.LocalModsDir, StringComparison.InvariantCultureIgnoreCase)) + ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); + if (contentPackage == null) { - string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); - if (contentPackage != null) - { - modProject = new ModProject(contentPackage); - fileListPath = contentPackage.Path; - EnqueueForReload(contentPackage); - } - } - - savePath = Path.Combine(prevDir, savePath).CleanUpPath(); - if (!isVanillaSub) - { - addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); + throw new InvalidOperationException($"Tried to overwrite a submarine ({name}) that's not in a local package!"); } + ModProject modProject = new ModProject(contentPackage); + packageToSaveTo = contentPackage; + savePath = prevSavePath; + addSubAndSaveModProject(modProject, savePath, contentPackage.Path); } else { - savePath = Path.Combine(newLocalModDir, savePath); - ModProject modProject = new ModProject() { Name = name }; + savePath = Path.Combine(newLocalModDir, savePath); + ModProject modProject = new ModProject { Name = name }; addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); -#if !DEBUG - if (vanilla != null) - { - string pathToCompare = savePath.Replace(@"\", @"/"); - if (vanillaSubs.Any(sub => sub.Value.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) - { - GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUIStyle.Red, font: GUIStyle.LargeFont); - return false; - } - } -#endif - if (MainSub != null) { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; @@ -1907,6 +1934,7 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); } } + if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; @@ -1935,12 +1963,7 @@ namespace Barotrauma SetMode(Mode.Default); } - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) - { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } - }; - - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1959,7 +1982,7 @@ namespace Barotrauma submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); - nameBox = new GUITextBox(new RectTransform(new Vector2(.95f, 0.05f), leftColumn.RectTransform)) + nameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)) { OnEnterPressed = ChangeSubName }; @@ -1976,7 +1999,7 @@ namespace Barotrauma return true; }; - nameBox.Text = subNameLabel?.Text?.SanitizedValue ?? ""; + nameBox.Text = MainSub?.Info.Name ?? ""; submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; @@ -2032,17 +2055,14 @@ namespace Barotrauma //--------------------------------------- - var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); + + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) { - IgnoreLayoutGroups = true, CanBeFocused = true, Visible = false, Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, outpostSettingsContainer.RectTransform), "InnerFrame") - { - IgnoreLayoutGroups = true - }; // module flags --------------------- @@ -2050,8 +2070,8 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); HashSet availableFlags = new HashSet(); - foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } + foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } @@ -2251,7 +2271,7 @@ namespace Barotrauma { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip") }; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), NumberType.Int) { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip"), IntValue = MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, @@ -2269,7 +2289,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), commonnessGroup.RectTransform), TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), NumberType.Float) { FloatValue = MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, MinValueFloat = 0, @@ -2281,15 +2301,76 @@ namespace Barotrauma }; outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); - //------------------------------------------------------------------ + //--------------------------------------- - var subSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), leftColumn.RectTransform)) + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + + // ------------------- + + var beaconMinDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, subSettingsContainer.RectTransform), "InnerFrame") + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), + TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMinDifficultyGroup.RectTransform), NumberType.Int) { - IgnoreLayoutGroups = true + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MinLevelDifficulty = numberInput.IntValue; + } + }; + beaconMinDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + var beaconMaxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), + TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) + { + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; + } + }; + beaconMaxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDamagedWalls = tb.Selected; + return true; + } + }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDisconnectedWires = tb.Selected; + return true; + } + }; + beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + + //------------------------------------------------------------------ + + var subSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + Stretch = true }; var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2299,8 +2380,9 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform), TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true); + int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, @@ -2315,7 +2397,7 @@ namespace Barotrauma MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice); } - var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -2323,19 +2405,27 @@ namespace Barotrauma TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); - classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); - classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); - classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); - classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); + foreach (SubmarineClass @class in Enum.GetValues(typeof(SubmarineClass))) + { + classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{@class}"), @class); + } + classDropDown.AddItem(TextManager.Get(nameof(SubmarineTag.Shuttle)), SubmarineTag.Shuttle); classDropDown.OnSelected += (selected, userdata) => { - SubmarineClass submarineClass = (SubmarineClass)userdata; - MainSub.Info.SubmarineClass = submarineClass; + switch (userdata) + { + case SubmarineClass submarineClass: + MainSub.Info.RemoveTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = submarineClass; + break; + case SubmarineTag.Shuttle: + MainSub.Info.AddTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = SubmarineClass.Undefined; + break; + } return true; }; - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - classText.Enabled = classDropDown.ButtonEnabled = !MainSub.Info.HasTag(SubmarineTag.Shuttle); + classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2345,13 +2435,13 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); - var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 }; new GUITextBlock(new RectTransform(new Vector2(0.06f, 1.0f), crewSizeArea.RectTransform), "-", textAlignment: Alignment.Center); - var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 @@ -2382,30 +2472,55 @@ namespace Barotrauma var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft"); var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), - text: crewExperienceLevels[0], textAlignment: Alignment.Center); + text: TextManager.Get(SubmarineInfo.CrewExperienceLevel.CrewExperienceLow.ToIdentifier()), textAlignment: Alignment.Center); var toggleExpRight = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleRight"); toggleExpLeft.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex--; - if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience--; + if (MainSub.Info.RecommendedCrewExperience < SubmarineInfo.CrewExperienceLevel.CrewExperienceLow) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; toggleExpRight.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex++; - if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience++; + if (MainSub.Info.RecommendedCrewExperience > SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; + + var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), hideInMenusArea.RectTransform), + TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + + new GUITickBox(new RectTransform((0.4f, 1.0f), hideInMenusArea.RectTransform), "") + { + Selected = MainSub.Info.HasTag(SubmarineTag.HideInMenus), + OnSelected = box => + { + if (box.Selected) + { + MainSub.Info.AddTag(SubmarineTag.HideInMenus); + } + else + { + MainSub.Info.RemoveTag(SubmarineTag.HideInMenus); + } + return true; + } + }; if (MainSub != null) { @@ -2413,9 +2528,11 @@ namespace Barotrauma int max = MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(MainSub.Info.RecommendedCrewExperience) ? - crewExperienceLevels[0] : MainSub.Info.RecommendedCrewExperience; - experienceText.Text = TextManager.Get((string)experienceText.UserData); + if (MainSub.Info.RecommendedCrewExperience == SubmarineInfo.CrewExperienceLevel.Unknown) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); } subTypeDropdown.OnSelected += (selected, userdata) => @@ -2426,12 +2543,14 @@ namespace Barotrauma { MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(MainSub.Info); } - previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); + else if (type == SubmarineType.BeaconStation) + { + MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info); + } + previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; - outpostSettingsContainer.IgnoreLayoutGroups = !outpostSettingsContainer.Visible; - + beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; subSettingsContainer.Visible = type == SubmarineType.Player; - subSettingsContainer.IgnoreLayoutGroups = !subSettingsContainer.Visible; return true; }; subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); @@ -2447,6 +2566,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageCreate"), style: "GUIButtonSmall") { + Enabled = MainSub?.Info.AllowPreviewImage ?? false, OnClicked = (btn, userdata) => { using (System.IO.MemoryStream imgStream = new System.IO.MemoryStream()) @@ -2464,6 +2584,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageBrowse"), style: "GUIButtonSmall") { + Enabled = MainSub?.Info.AllowPreviewImage ?? false, OnClicked = (btn, userdata) => { FileSelection.OnFileSelected = (file) => @@ -2492,77 +2613,144 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); - var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), - TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUIStyle.SmallFont); - - var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), - horizontalArea.RectTransform, Anchor.BottomLeft), - style: "InnerFrame"); - - foreach (SubmarineTag tag in Enum.GetValues(typeof(SubmarineTag))) + GUIButton createTabberBtn(string labelTag) { - LocalizedString tagStr = TextManager.Get(tag.ToString()); - var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), - tagStr, font: GUIStyle.SmallFont) - { - Selected = MainSub != null && MainSub.Info.HasTag(tag), - UserData = tag, - OnSelected = (GUITickBox tickBox) => - { - if (MainSub == null) return false; - SubmarineTag tag = (SubmarineTag)tickBox.UserData; - if (tag == SubmarineTag.Shuttle) - { - if (tickBox.Selected) - { - classDropDown.SelectItem(SubmarineClass.Undefined); - } - else - { - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - } - classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; - } - if (tickBox.Selected) - { - MainSub.Info.AddTag(tag); - } - else - { - MainSub.Info.RemoveTag(tag); - } - return true; - } - }; + var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.RectTransform.MaxSize = RectTransform.MaxPoint; + btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + btn.Font = GUIStyle.SmallFont; + return btn; } - var contentPackagesLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), + var saveToPackageTabBtn = createTabberBtn("SaveToLocalPackage"); + saveToPackageTabBtn.Selected = true; + var reqPackagesTabBtn = createTabberBtn("RequiredContentPackages"); + reqPackagesTabBtn.Selected = false; + + var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + + var saveInPackageLayout = new GUILayoutGroup(new RectTransform(Vector2.One, horizontalArea.RectTransform, Anchor.BottomRight)) { Stretch = true }; - var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), - TextManager.Get("RequiredContentPackages"), wrap: true, font: GUIStyle.SmallFont); - contentPackagesLabel.RectTransform.MinSize - = GUIStyle.SmallFont.MeasureString(contentPackagesLabel.WrappedText).ToPoint(); + var packageToSaveInList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + saveInPackageLayout.RectTransform)); - var contentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), - contentPackagesLayout.RectTransform)); + var packToSaveInFilter + = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform), + createClearButton: true); + GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p) + { + var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = p + }; + if (p != null && p != ContentPackageManager.VanillaCorePackage) { listItem.ToolTip = p.Dir; } + var retVal = + new GUILayoutGroup(new RectTransform(Vector2.One, listItem.RectTransform), + isHorizontal: true) { Stretch = true }; + var iconFrame = + new GUIFrame( + new RectTransform(Vector2.One, retVal.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null) { CanBeFocused = false }; + var pkgText = new GUITextBlock(new RectTransform(Vector2.One, retVal.RectTransform), itemText) + { CanBeFocused = false }; + return retVal; + } + +#if DEBUG + //this is a debug-only option so I won't bother submitting it for localization + var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage); + var modifyVanillaListIcon = modifyVanillaListItem.GetChild(); + GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton"); +#endif + + var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null); + var newPackageListIcon = newPackageListItem.GetChild(); + var newPackageListText = newPackageListItem.GetChild(); + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), + onUpdate: (f, component) => + { + bool canCreateNewPackage = true; + foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) + { + contentChild.Visible = !(contentChild.UserData is ContentPackage p) + || !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase); + canCreateNewPackage &= contentChild.Visible; + contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && + !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); + } + + if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + newPackageListText.Text = TextManager.Get("CreateNewLocalPackage"); + } + if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton"); + newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text); + } + }); + packageToSaveInList.Select(0); + ContentPackage ownerPkg = null; + if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } + foreach (var p in ContentPackageManager.LocalPackages) + { + addItemToPackageToSaveList(p.Name, p); + } + + if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase)) + { + packageToSaveInList.Select(ownerPkg); + packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); + } + + var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, + horizontalArea.RectTransform, Anchor.BottomRight)) + { + Stretch = true, + Visible = false + }; + + var requiredContentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + requiredContentPackagesLayout.RectTransform)); + + var filterLayout = new GUILayoutGroup( + new RectTransform((1.0f, 0.15f), requiredContentPackagesLayout.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + var contentPackFilter - = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), + = new GUITextBox(new RectTransform((0.6f, 1.0f), filterLayout.RectTransform), createClearButton: true); contentPackFilter.OnTextChanged += (box, text) => { - contentPackList.Content.Children.ForEach(c + requiredContentPackList.Content.Children.ForEach(c => c.Visible = !(c is GUITickBox tb && !tb.Text.Contains(text, StringComparison.OrdinalIgnoreCase))); return true; }; + var autoDetectBtn = new GUIButton(new RectTransform((0.4f, 1.0f), filterLayout.RectTransform), + text: TextManager.Get("AutoDetectRequiredPackages"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage) + .Distinct().OfType().Select(p => p.Name).ToHashSet(); + var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); + tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? "")); + return false; + } + }; + if (MainSub != null) { List allContentPacks = MainSub.Info.RequiredContentPackages.ToList(); @@ -2590,7 +2778,7 @@ namespace Barotrauma foreach (string contentPackageName in allContentPacks) { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), requiredContentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) { Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName @@ -2610,7 +2798,19 @@ namespace Barotrauma } } + GUIButton.OnClickedHandler switchToTab(GUIButton tabBtn, GUIComponent tab) + => (button, obj) => + { + horizontalArea.Children.ForEach(c => c.Visible = false); + contentPackageTabber.Children.ForEach(c => c.Selected = false); + tabBtn.Selected = true; + tab.Visible = true; + return false; + }; + saveToPackageTabBtn.OnClicked = switchToTab(saveToPackageTabBtn, saveInPackageLayout); + reqPackagesTabBtn.OnClicked = switchToTab(reqPackagesTabBtn, requiredContentPackagesLayout); + var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedSaveFrame.RectTransform, Anchor.BottomCenter, minSize: new Point(0, 30)), style: null); var cancelButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), @@ -2626,22 +2826,23 @@ namespace Barotrauma var saveButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight), TextManager.Get("SaveSubButton")) { - OnClicked = SaveSub + OnClicked = (button, o) => SaveSub(packageToSaveInList.SelectedData as ContentPackage) }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = + subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize = new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostSettingsContainer.Rect.Height)); subSettingsContainer.Recalculate(); outpostSettingsContainer.Recalculate(); + beaconSettingsContainer.Recalculate(); descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; subTypeDropdown.SelectItem(MainSub.Info.Type); - if (quickSave) { SaveSub(saveButton, saveButton.UserData); } + if (quickSave) { SaveSub(null); } } private void CreateSaveAssemblyScreen() @@ -2750,6 +2951,8 @@ namespace Barotrauma } } + nameBox.Text = nameBox.Text.Trim(); + bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); @@ -2766,16 +2969,7 @@ namespace Barotrauma } else { - var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); - var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); - if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath.Value) == ItemAssemblyPrefab.VanillaSaveFolder) - { - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); - } - else - { - Save(); - } + Save(); } void Save() @@ -2784,7 +2978,7 @@ namespace Barotrauma if (existingContentPackage == null) { //content package doesn't exist, create one - ModProject modProject = new ModProject() { Name = nameBox.Text }; + ModProject modProject = new ModProject { Name = nameBox.Text }; var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); @@ -2845,6 +3039,13 @@ namespace Barotrauma } } + private IEnumerable GetLoadableSubs() + { + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + return SubmarineInfo.SavedSubmarines.Where(s + => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder); + } + private void CreateLoadScreen() { CloseItem(); @@ -2879,13 +3080,36 @@ namespace Barotrauma var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (GUIComponent selected, object userData) => { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { - deleteBtn.Enabled = userData is SubmarineInfo subInfo - && GetContentPackageIntrinsicallyTiedToSub(subInfo) != null; + deleteBtn.ToolTip = string.Empty; + if (!(userData is SubmarineInfo subInfo)) + { + deleteBtn.Enabled = false; + return true; + } + + var package = GetLocalPackageThatOwnsSub(subInfo); + if (package != null) + { + deleteBtn.Enabled = true; + } + else + { + deleteBtn.Enabled = false; + if (IsVanillaSub(subInfo)) + { + deleteBtn.ToolTip = TextManager.Get("cantdeletevanillasub"); + } + else if (GetPackageThatOwnsSub(subInfo, ContentPackageManager.AllPackages) is ContentPackage subPackage) + { + deleteBtn.ToolTip = TextManager.GetWithVariable("cantdeletemodsub", "[modname]", subPackage.Name); + } + } } return true; } @@ -2895,9 +3119,10 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - List sortedSubs = new List(SubmarineInfo.SavedSubmarines.Where(s => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder)); - sortedSubs.Sort((s1, s2) => { return s1.Type.CompareTo(s2.Type) * 100 + s1.Name.CompareTo(s2.Name); }); + var sortedSubs = GetLoadableSubs() + .OrderBy(s => s.Type) + .ThenBy(s => s.Name) + .ToList(); SubmarineInfo prevSub = null; @@ -2918,13 +3143,39 @@ namespace Barotrauma prevSub = sub; } + string pathWithoutUserName = Path.GetFullPath(sub.FilePath); + string saveFolder = Path.GetFullPath(SaveUtil.SaveFolder); + if (pathWithoutUserName.StartsWith(saveFolder)) + { + pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; + } + else + { + pathWithoutUserName = sub.FilePath; + } + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) { UserData = sub, - ToolTip = sub.FilePath + ToolTip = pathWithoutUserName }; + if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) + { + if (GetLocalPackageThatOwnsSub(sub) == null && + ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) + { + //workshop mod + textBlock.OverrideTextColor(Color.MediumPurple); + } + else + { + //local mod + textBlock.OverrideTextColor(GUIStyle.TextColorBright); + } + } + if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), @@ -3024,7 +3275,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), controlBtnHolder.RectTransform, Anchor.BottomRight), TextManager.Get("Load")) { - OnClicked = LoadSub + OnClicked = HitLoadSubButton }; controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height); @@ -3037,15 +3288,33 @@ namespace Barotrauma if (!(child.UserData is SubmarineInfo sub)) { continue; } child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower()); } + + //go through the elements backwards, and disable the labels for sub categories if there's no subs visible in them + bool subVisibleInCategory = false; + foreach (GUIComponent child in subList.Content.Children.Reverse()) + { + if (!(child.UserData is SubmarineInfo sub)) + { + if (child.Enabled) + { + child.Visible = subVisibleInCategory; + } + subVisibleInCategory = false; + } + else + { + subVisibleInCategory |= child.Visible; + } + } } /// /// Recovers the auto saved submarine /// /// - private void LoadAutoSave(object UserData) + private void LoadAutoSave(object userData) { - if (!(UserData is XElement element)) { return; } + if (!(userData is XElement element)) { return; } #warning TODO: revise string filePath = element.GetAttributeStringUnrestricted("file", ""); @@ -3078,7 +3347,7 @@ namespace Barotrauma loadFrame = null; } - private bool LoadSub(GUIButton button, object obj) + private bool HitLoadSubButton(GUIButton button, object obj) { if (loadFrame == null) { @@ -3093,14 +3362,68 @@ namespace Barotrauma return false; } - if (subList.SelectedComponent == null) { return false; } - if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } + if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; } - LoadSub(selectedSubInfo); - - return true; + var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); + if (ownerPackage is null) + { + if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) + { + if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) + { + AskLoadPublishedSub(selectedSubInfo, workshopPackage); + } + else + { + AskLoadSubscribedSub(selectedSubInfo); + } + } + else if (IsVanillaSub(selectedSubInfo)) + { +#if DEBUG + LoadSub(selectedSubInfo); +#else + AskLoadVanillaSub(selectedSubInfo); +#endif + } + } + else + { + LoadSub(selectedSubInfo); + } + return false; } + void AskLoadSub(SubmarineInfo info, LocalizedString header, LocalizedString desc) + { + var msgBox = new GUIMessageBox( + header, + desc, + new[] { TextManager.Get("LoadAnyway"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked = (button, o) => + { + LoadSub(info); + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + void AskLoadPublishedSub(SubmarineInfo info, ContentPackage pkg) + => AskLoadSub(info, + TextManager.Get("LoadingPublishedSubmarineHeader"), + TextManager.GetWithVariable("LoadingPublishedSubmarineDesc", "[modname]", pkg.Name)); + + void AskLoadSubscribedSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingSubscribedSubmarineHeader"), + TextManager.Get("LoadingSubscribedSubmarineDesc")); + + void AskLoadVanillaSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingVanillaSubmarineHeader"), + TextManager.Get("LoadingVanillaSubmarineDesc")); + public void LoadSub(SubmarineInfo info) { Submarine.Unload(); @@ -3128,7 +3451,10 @@ namespace Barotrauma { if (item.ParentInventory != null || item.body != null) continue; var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + foreach (var light in item.GetComponents()) + { + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); + } } new GUIMessageBox("", TextManager.Get("AdjustedLightsNotification")); return true; @@ -3139,11 +3465,18 @@ namespace Barotrauma ReconstructLayers(); } - private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) - => ContentPackageManager.LocalPackages.Regular - .Where(p => p.Files.Length == 1) - .FirstOrDefault(regularPackage => regularPackage.Files[0].Path == sub.FilePath); + private static ContentPackage GetPackageThatOwnsSub(SubmarineInfo sub, IEnumerable packages) + => packages.FirstOrDefault(package => package.Files.Any(f => f.Path == sub.FilePath)); + private static ContentPackage GetLocalPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.LocalPackages); + + private static ContentPackage GetWorkshopPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.WorkshopPackages); + + private static bool IsVanillaSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.VanillaCorePackage.ToEnumerable()) != null; + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } @@ -3151,7 +3484,7 @@ namespace Barotrauma //If the sub is included in a content package that only defines that one sub, //check that it's a local content package and only allow deletion if it is. //(deleting from the Submarines folder is also currently allowed, but this is temporary) - var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); + var subPackage = GetLocalPackageThatOwnsSub(sub); if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } var msgBox = new GUIMessageBox( @@ -3164,9 +3497,11 @@ namespace Barotrauma { if (subPackage != null) { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); - ContentPackageManager.LocalPackages.Refresh(); - ContentPackageManager.EnabledPackages.DisableRemovedMods(); + File.Delete(sub.FilePath); + ModProject modProject = new ModProject(subPackage); + modProject.RemoveFile(modProject.Files.First(f => ContentPath.FromRaw(subPackage, f.Path) == sub.FilePath)); + modProject.Save(subPackage.Path); + ReloadModifiedPackage(subPackage); } sub.Dispose(); @@ -3605,17 +3940,17 @@ namespace Barotrauma GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; - GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; - GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; - GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; @@ -3795,6 +4130,7 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, OnSelected = SelectWire }; @@ -4377,8 +4713,7 @@ namespace Barotrauma { Rectangle hullRect = rect; hullRect.Y = -hullRect.Y; - Hull newHull = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), - hullRect, + Hull newHull = new Hull(hullRect, MainSub); } @@ -4390,7 +4725,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), gapRect); + new Gap(gapRect); } } @@ -4773,7 +5108,7 @@ namespace Barotrauma int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; if (index > -1 && index < listBox.Content.CountChildren) { - listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); + listBox.Select(index); SkipInventorySlotUpdate = true; } } @@ -4828,7 +5163,7 @@ namespace Barotrauma } } - if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default) + if (PlayerInput.KeyHit(Keys.Q) && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index af4eba036..afd88b6da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -59,7 +59,7 @@ namespace Barotrauma { if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { numInput.FloatValue = f; if (flash) @@ -76,7 +76,7 @@ namespace Barotrauma { if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { numInput.IntValue = integer; if (flash) @@ -127,7 +127,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { numInput.FloatValue = i == 0 ? v2.X : v2.Y; if (flash) @@ -145,7 +145,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { switch (i) { @@ -174,7 +174,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { switch (i) { @@ -206,7 +206,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { switch (i) { @@ -246,7 +246,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { switch (i) { @@ -517,7 +517,7 @@ namespace Barotrauma } else { - var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), NumberType.Int) { ToolTip = toolTip, Font = GUIStyle.SmallFont @@ -554,7 +554,7 @@ namespace Barotrauma }; GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, - Anchor.TopRight), GUINumberInput.NumberType.Float) + Anchor.TopRight), NumberType.Float) { ToolTip = toolTip, Font = GUIStyle.SmallFont @@ -770,7 +770,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -838,7 +838,7 @@ namespace Barotrauma } new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -909,7 +909,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -985,7 +985,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -1078,7 +1078,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.ColorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -1153,7 +1153,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -1282,6 +1282,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { string text = userData as string ?? ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 9a1a30d8a..4e9d8b6a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -36,6 +37,8 @@ namespace Barotrauma public readonly WorkshopMenu WorkshopMenu; + private static readonly ImmutableHashSet LegacyInputTypes = new List() { InputType.Chat, InputType.RadioChat }.ToImmutableHashSet(); + public static SettingsMenu Create(RectTransform mainParent) { Instance?.Close(); @@ -171,7 +174,7 @@ namespace Barotrauma int childIndex = values.IndexOf(currentValue); dropdown.Select(childIndex); dropdown.ListBox.ForceLayoutRecalculation(); - dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex), playSound: false); + dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex)); dropdown.OnSelected = (dd, obj) => { setter((T)obj); @@ -182,7 +185,7 @@ namespace Barotrauma private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); - var slider = new GUIScrollBar(new RectTransform((0.82f, 1.0f), layout.RectTransform), style: "GUISlider") + var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") { Range = range, BarScrollValue = currentValue, @@ -193,7 +196,7 @@ namespace Barotrauma { slider.ToolTip = tooltip; } - var label = new GUITextBlock(new RectTransform((0.18f, 1.0f), layout.RectTransform), + var label = new GUITextBlock(new RectTransform((0.28f, 1.0f), layout.RectTransform), labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); slider.OnMoved = (sb, val) => { @@ -217,10 +220,7 @@ namespace Barotrauma }; } - private string ScaleResolution(float scale) => - $"{Round(unsavedConfig.Graphics.Width * scale)}\nx\n{Round(unsavedConfig.Graphics.Height * scale)}"; - - private string Percentage(float v) => $"{Round(v * 100)}%"; + private string Percentage(float v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value; private int Round(float v) => (int)MathF.Round(v); @@ -259,22 +259,27 @@ namespace Barotrauma Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, (v) => unsavedConfig.Graphics.VSync = v); Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, (v) => unsavedConfig.Graphics.CompressTextures = v); - Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); - Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); - Spacer(right); - Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, (v) => unsavedConfig.Graphics.LosMode = v); Spacer(right); - + Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); - Slider(right, (0.5f, 1.0f), 10, ScaleResolution, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Slider(right, (0.5f, 1.0f), 11, (v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); - + + Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); + Slider(right, (10, 210), 21, (v) => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + (v) => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Spacer(right); + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + + Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); + Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Spacer(right); } - + private static string TrimAudioDeviceName(string name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } @@ -390,6 +395,9 @@ namespace Barotrauma Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, (v) => unsavedConfig.Audio.UiVolume = v); + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); @@ -454,6 +462,8 @@ namespace Barotrauma Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } + + private bool inputBoxSelectedThisFrame = false; private void CreateControlsTab() { GUIFrame content = CreateNewContentFrame(Tab.Controls); @@ -479,10 +489,14 @@ namespace Barotrauma HashSet inputButtons = new HashSet(); Action? currentSetter = null; - void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter) + void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter, bool isLegacyBind = false) { var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform), style: null); + if (isLegacyBind) + { + labelText = TextManager.GetWithVariable("legacyitemformat", "[name]", labelText); + } var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText, font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes}; var inputBox = new GUIButton( @@ -498,21 +512,24 @@ namespace Barotrauma bool willBeSelected = !btn.Selected; if (willBeSelected) { + inputBoxSelectedThisFrame = true; currentSetter = (v) => { valueSetter(v); btn.Text = valueNameGetter(); }; } - else - { - currentSetter = null; - } btn.Selected = willBeSelected; return true; } }; + if (isLegacyBind) + { + label.TextColor = Color.Lerp(label.TextColor, label.DisabledTextColor, 0.5f); + inputBox.Color = Color.Lerp(inputBox.Color, inputBox.DisabledColor, 0.5f); + inputBox.TextColor = Color.Lerp(inputBox.TextColor, label.DisabledTextColor, 0.5f); + } inputButtons.Add(inputBox); } @@ -520,6 +537,12 @@ namespace Barotrauma { if (currentSetter is null) { return; } + if (PlayerInput.PrimaryMouseButtonClicked() && inputBoxSelectedThisFrame) + { + inputBoxSelectedThisFrame = false; + return; + } + void clearSetter() { currentSetter = null; @@ -533,7 +556,7 @@ namespace Barotrauma } var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); - if ((pressedKeys?.Any() ?? false)) + if (pressedKeys?.Any() ?? false) { if (pressedKeys.Contains(Keys.Escape)) { @@ -544,7 +567,8 @@ namespace Barotrauma callSetter(pressedKeys.First()); } } - else if (PlayerInput.PrimaryMouseButtonClicked() && !(GUI.MouseOn is GUIButton)) + else if (PlayerInput.PrimaryMouseButtonClicked() && + (GUI.MouseOn == null || !(GUI.MouseOn is GUIButton) || GUI.MouseOn.IsChildOf(keyMapList.Content))) { callSetter(MouseButton.PrimaryMouse); } @@ -592,7 +616,8 @@ namespace Barotrauma currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, - (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v)); + (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), + LegacyInputTypes.Contains(input)); } } @@ -607,30 +632,31 @@ namespace Barotrauma var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex]; addInputToRow( currRow, - TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex+1).ToString(CultureInfo.InvariantCulture)), + TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } GUILayoutGroup resetControlsHolder = - new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true) + new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center) { RelativeSpacing = 0.1f }; var defaultBindingsButton = new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), - TextManager.Get("SetDefaultBindings"), style: "GUIButtonSmall") + TextManager.Get("Reset"), style: "GUIButtonSmall") { - ToolTip = TextManager.Get("SetDefaultBindingsTooltip") - }; - - var legacyBindingsButton = - new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), - TextManager.Get("SetLegacyBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetLegacyBindingsTooltip") + ToolTip = TextManager.Get("SetDefaultBindingsTooltip"), + OnClicked = (btn, userdata) => + { + unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); + unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + Create(mainFrame.Parent.RectTransform); + Instance?.SelectTab(Tab.Controls); + return true; + } }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 7fe005bb5..a2418f455 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -705,7 +705,7 @@ namespace Barotrauma.Sounds public void ApplySettings() { SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume, 0); - SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.UiVolume, 0); SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index c7d65c9ca..0705c3084 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -233,16 +233,18 @@ namespace Barotrauma float dist = diff.Length(); float distFallOff = dist / FlowSoundRange; - if (distFallOff >= 0.99f) continue; + if (distFallOff >= 0.99f) { continue; } + + float gain = MathHelper.Clamp(gapFlow / 100.0f, 0.0f, 1.0f); //flow at the left side if (diff.X < 0) { - targetFlowLeft[flowSoundIndex] += 1.0f - distFallOff; + targetFlowLeft[flowSoundIndex] += gain - distFallOff; } else { - targetFlowRight[flowSoundIndex] += 1.0f - distFallOff; + targetFlowRight[flowSoundIndex] += gain - distFallOff; } } } @@ -287,7 +289,7 @@ namespace Barotrauma flowSoundChannels[i] = FlowSounds[i].Sound.Play(1.0f, FlowSoundRange, soundPos); flowSoundChannels[i].Looping = true; } - flowSoundChannels[i].Gain = Math.Min(Math.Max(flowVolumeRight[i], flowVolumeLeft[i]), 1.0f); + flowSoundChannels[i].Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]); flowSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } @@ -416,7 +418,7 @@ namespace Barotrauma } else { - if (!Level.IsLoadedOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && + if (!Level.IsLoadedFriendlyOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && sub.Info != null && !sub.Info.IsOutpost) { hullSoundSource = Character.Controlled.CurrentHull; @@ -853,7 +855,7 @@ namespace Barotrauma if (SplashSounds.Count == 0) { return; } int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; - var channel = SplashSounds[splashIndex].Sound.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); + SplashSounds[splashIndex].Sound?.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } public static void PlayDamageSound(string damageType, float damage, PhysicsBody body) @@ -887,5 +889,13 @@ namespace Barotrauma .Where(s => s.Type == soundType) .GetRandomUnsynced()?.Sound?.Play(null, "ui"); } + + public static void PlayUISound(GUISoundType? soundType) + { + if (soundType.HasValue) + { + PlayUISound(soundType.Value); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 9eab19935..14b0b68e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -202,11 +202,6 @@ namespace Barotrauma { Sound?.Dispose(); Sound = null; } - - ~SoundPrefab() - { - Dispose(); - } } [TagNames("damagesound")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 6db48e03d..2a71f2bd7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -39,9 +39,8 @@ namespace Barotrauma get { return texture != null && !cannotBeLoaded; } } - public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation) + public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation, other.FilePath.Value) { - FilePath = other.FilePath; Compress = other.Compress; size = other.size; effects = other.effects; @@ -58,6 +57,17 @@ namespace Barotrauma rotation = newRotation; FilePath = ContentPath.FromRaw(path); AddToList(this); + if (!string.IsNullOrEmpty(path)) + { + Identifier fullPath = Path.GetFullPath(path).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier(); + lock (list) + { + if (!textureRefCounts.TryAdd(fullPath, new TextureRefCounter { RefCount = 1, Texture = texture })) + { + textureRefCounts[fullPath].RefCount++; + } + } + } } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) @@ -243,12 +253,12 @@ namespace Barotrauma if (flipHorizontal) { float diff = targetSize.X % (sourceRect.Width * scale.X); - flippedDrawOffset.X = (int)((sourceRect.Width * scale.X - diff) / scale.X); + flippedDrawOffset.X = (int)MathF.Round((sourceRect.Width * scale.X - diff) / scale.X); } if (flipVertical) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); - flippedDrawOffset.Y = (int)((sourceRect.Height * scale.Y - diff) / scale.Y); + flippedDrawOffset.Y = (int)MathF.Round((sourceRect.Height * scale.Y - diff) / scale.Y); } drawOffset += flippedDrawOffset; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index b8f6328f8..ddac26c05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -119,9 +119,11 @@ namespace Barotrauma.Steam onComplete?.Invoke(); } msgBox.Close(); + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) { - mutableWorkshopMenu.PopulateInstalledModLists(); + mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 37a04c2a0..a21ef24c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -16,6 +16,8 @@ namespace Barotrauma.Steam { public static partial class Workshop { + public const int MaxThumbnailSize = 1024 * 1024; + public static readonly ImmutableArray Tags = new [] { "submarine", @@ -89,11 +91,6 @@ namespace Barotrauma.Steam } } - ~ItemThumbnail() - { - Dispose(); - } - public void Dispose() { if (ItemId == 0) { return; } @@ -177,14 +174,17 @@ namespace Barotrauma.Steam DeletePublishStagingCopy(); Directory.CreateDirectory(PublishStagingDir); - await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); + var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); + ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded"); + //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be - ModProject modProject = new ModProject(contentPackage) + ModProject modProject = new ModProject(tempPkg) { ModVersion = modVersion }; - modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); + modProject.Save(stagingFileListPath); } public static async Task CreateLocalCopy(ContentPackage contentPackage) @@ -218,7 +218,7 @@ namespace Barotrauma.Steam throw new Exception($"{newPath} already exists"); } - await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath, ShouldCorrectPaths.Yes); ModProject modProject = new ModProject(contentPackage); modProject.DiscardHashAndInstallTime(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 484fe3182..7cc29acce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -46,7 +46,10 @@ namespace Barotrauma.Steam regularBox.CanBeFocused = true; } } - filterBox = CreateSearchBox(mainLayout, width: 1.0f); + + var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); + searchRectT.RelativeSize = (1.0f, searchRectT.RelativeSize.Y); + filterBox = CreateSearchBox(searchRectT); Label(mainLayout, TextManager.Get("CannotChangeMods"), GUIStyle.Font); } @@ -55,9 +58,8 @@ namespace Barotrauma.Steam { string str = filterBox.Text; regularList.Content.Children - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs new file mode 100644 index 000000000..2e5953ae5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -0,0 +1,746 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + + private readonly GUIDropDown enabledCoreDropdown; + private readonly GUIListBox enabledRegularModsList; + private readonly GUIListBox disabledRegularModsList; + private readonly Action onInstalledInfoButtonHit; + private readonly GUITextBox modsListFilter; + private readonly Dictionary modsListFilterTickboxes; + private readonly GUIButton bulkUpdateButton; + + private GUIComponent? draggedElement = null; + private GUIListBox? draggedElementOrigin = null; + + private void UpdateSubscribedModInstalls() + { + if (!SteamManager.IsInitialized) { return; } + + uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); + if (numSubscribedMods == memSubscribedModCount) { return; } + memSubscribedModCount = numSubscribedMods; + + var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); + var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); + foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(item); + } + } + + TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => + { + if (!t.TryGetResult(out ISet publishedItems)) { return; } + + var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); + bool needsRefresh = false; + foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + SteamManager.Workshop.Uninstall(item); + needsRefresh = true; + } + + if (needsRefresh) + { + PopulateInstalledModLists(); + } + }); + } + + private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( + GUIComponent parent, + float leftWidth = 0.3875f, + float centerWidth = 0.025f, + float rightWidth = 0.5875f, + bool split = false, + float height = 1.0f) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); + var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, center, right); + } + + private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + { + if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) + { + //move the dragged elements to the index determined previously + var draggedElement = from.DraggedElement; + + var selected = from.AllSelected.ToList(); + selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); + + float oldCount = to.Content.CountChildren; + float newCount = oldCount + selected.Count; + + var offset = draggedElement.RectTransform.AbsoluteOffset; + offset += from.Content.Rect.Location; + offset -= to.Content.Rect.Location; + + for (int i = 0; i < selected.Count; i++) + { + var c = selected[i]; + c.Parent.RemoveChild(c); + c.RectTransform.Parent = to.Content.RectTransform; + c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); + } + + from.DraggedElement = null; + from.Deselect(); + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + + //recalculate the dragged element's offset so it doesn't jump around + draggedElement.RectTransform.AbsoluteOffset = offset; + + to.DraggedElement = draggedElement; + + to.BarScroll *= (oldCount / newCount); + } + } + + private Action? currentSwapFunc = null; + private GUISoundType? swapSoundType = null; + + private void PlaySwapSound() + { + SoundPlayer.PlayUISound(swapSoundType); + } + + private void SetSwapFunc(GUIListBox from, GUIListBox to) + { + currentSwapFunc = () => + { + to.Deselect(); + var selected = from.AllSelected.ToArray(); + foreach (var frame in selected) + { + frame.Parent.RemoveChild(frame); + frame.RectTransform.Parent = to.Content.RectTransform; + } + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + }; + + if (to == enabledRegularModsList) + { + swapSoundType = GUISoundType.Increase; + } + else if (to == disabledRegularModsList) + { + swapSoundType = GUISoundType.Decrease; + } + else + { + swapSoundType = null; + } + } + + private void CreateInstalledModsTab( + out GUIDropDown enabledCoreDropdown, + out GUIListBox enabledRegularModsList, + out GUIListBox disabledRegularModsList, + out Action onInstalledInfoButtonHit, + out GUITextBox modsListFilter, + out Dictionary modsListFilterTickboxes, + out GUIButton bulkUpdateButton) + { + GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); + + CreateWorkshopItemDetailContainer( + content, + out var outerContainer, + onSelected: (itemOrPackage, selectedFrame) => + { + if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } + }, + onDeselected: () => PopulateInstalledModLists(), + out onInstalledInfoButtonHit, out var deselect); + + GUILayoutGroup mainLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + mainLayout.RectTransform.SetAsFirstChild(); + + var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); + topLeft.Stretch = true; + Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); + enabledCoreDropdown = Dropdown(topLeft, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }, + heightScale: 1.0f / 13.0f); + Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); + topRight.ChildAnchor = Anchor.CenterLeft; + + var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + void padTopRight(float width=1.0f) + { + new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); + } + + padTopRight(); + //TODO: put stuff here + padTopRight(width: 3.0f); + var refreshListsButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIReloadButton") + { + OnClicked = (b, o) => + { + PopulateInstalledModLists(); + return false; + }, + ToolTip = TextManager.Get("RefreshModLists") + }; + bulkUpdateButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIUpdateButton") + { + OnClicked = (b, o) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Enabled = false + }; + padTopRight(width: 0.1f); + + var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); + right.ChildAnchor = Anchor.TopRight; + + //enabled mods + Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Increase, + }; + enabledRegularModsList = enabledModsList; + + //disabled mods + Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); + var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Decrease, + }; + disabledRegularModsList = disabledModsList; + + var centerButton = + new GUIButton( + new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, + anchor: Anchor.Center), + style: "GUIButtonToggleLeft") + { + PlaySoundOnSelect = false, + Visible = false, + OnClicked = (button, o) => + { + if (currentSwapFunc != null) + { + PlaySwapSound(); + currentSwapFunc.Invoke(); + } + return false; + } + }; + + enabledModsList.OnSelected = (frame, o) => + { + disabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); + + SetSwapFunc(enabledModsList, disabledModsList); + + return true; + }; + disabledModsList.OnSelected = (frame, o) => + { + enabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); + + SetSwapFunc(disabledModsList, enabledModsList); + + return true; + }; + + var filterContainer = new GUILayoutGroup(NewItemRectT(mainLayout, heightScale: 1.0f), isHorizontal: true) + { Stretch = true, RelativeSpacing = 0.01f }; + + void padFilterContainer(float width = 0.25f) + => new GUIFrame(new RectTransform((width, 1.0f), filterContainer!.RectTransform), style: null); + + GUIButton filterLayoutButton(string style) + => new GUIButton( + new RectTransform(Vector2.One, filterContainer!.RectTransform, scaleBasis: ScaleBasis.BothHeight), + "", style: style); + + padFilterContainer(width: 0.2f); + var loadPresetBtn = filterLayoutButton("OpenButton"); + loadPresetBtn.ToolTip = TextManager.Get("LoadModListPresetHeader"); + loadPresetBtn.OnClicked = OpenLoadPreset; + var savePresetBtn = filterLayoutButton("SaveButton"); + savePresetBtn.ToolTip = TextManager.Get("SaveModListPresetHeader"); + savePresetBtn.OnClicked = OpenSavePreset; + padFilterContainer(width: 0.05f); + var searchRectT = new RectTransform((0.5f, 1.0f), filterContainer.RectTransform); + var searchBox = CreateSearchBox(searchRectT); + modsListFilter = searchBox; + + var filterTickboxes = new Dictionary(); + modsListFilterTickboxes = filterTickboxes; + + var filterTickboxesDropdown + = filterLayoutButton("SetupVisibilityButton"); + var filterTickboxesContainer + = new GUIFrame(new RectTransform((0.3f, 0.2f), content.RectTransform, + scaleBasis: ScaleBasis.BothWidth), style: "InnerFrame"); + var filterTickboxesUpdater + = new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + filterTickboxesContainer.Visible = filterTickboxesDropdown.Selected; + filterTickboxesContainer.RectTransform.AbsoluteOffset + = (filterTickboxesDropdown.Rect.Location - content.Rect.Location) + + (filterTickboxesDropdown.Rect.Width / 2, 0) + - (filterTickboxesContainer.Rect.Size.ToVector2() * (0.5f, 1.0f)).ToPoint(); + filterTickboxesContainer.RectTransform.NonScaledSize + = new Point(filterTickboxes.Select(tb => (int)tb.Value.Font.MeasureString(tb.Value.GetChild().Text).X).Max(), + filterTickboxes.Select(tb => tb.Value.Rect.Height).Aggregate((a,b) => a+b)) + +(filterTickboxes.Values.First().Rect.Height * 4, filterTickboxes.Values.First().Rect.Height / 2); + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(filterTickboxesDropdown) + && !GUI.IsMouseOn(filterTickboxesContainer)) + { + filterTickboxesDropdown.Selected = false; + } + }); + + var filterTickboxesLayout + = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, filterTickboxesContainer.RectTransform, Anchor.Center)); + + void addFilterTickbox(Filter filter, string? style, bool selected) + { + var tickbox = new GUITickBox(NewItemRectT(filterTickboxesLayout!, heightScale: 0.5f), "") + { + Selected = selected, + OnSelected = _ => + { + UpdateModListItemVisibility(); + return true; + } + }; + filterTickboxes!.Add(filter, tickbox); + var text = new GUITextBlock(new RectTransform((1.0f, 1.0f), tickbox.RectTransform, Anchor.CenterRight) + { + AbsoluteOffset = (-tickbox.Box.Rect.Width * 2, 0), + }, + TextManager.Get($"ModFilter.{filter}")) + { + CanBeFocused = false + }; + var icon = new GUIFrame( + new RectTransform(Vector2.One, text.RectTransform, Anchor.CenterLeft, Pivot.CenterRight, + scaleBasis: ScaleBasis.BothHeight), style: style) + { + CanBeFocused = false + }; + } + + addFilterTickbox(Filter.ShowLocal, "WorkshopMenu.EditButton", selected: true); + addFilterTickbox(Filter.ShowWorkshop, "WorkshopMenu.DownloadedIcon", selected: true); + addFilterTickbox(Filter.ShowPublished, "WorkshopMenu.PublishedIcon", selected: true); + addFilterTickbox(Filter.ShowOnlySubs, null, selected: false); + addFilterTickbox(Filter.ShowOnlyItemAssemblies, null, selected: false); + + padFilterContainer(); + + new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + HandleDraggingAcrossModLists(enabledModsList, disabledModsList); + HandleDraggingAcrossModLists(disabledModsList, enabledModsList); + UpdateDraggingSounds(); + + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(enabledModsList) + && !GUI.IsMouseOn(disabledModsList) + && GUIContextMenu.CurrentContextMenu is null) + { + enabledModsList.Deselect(); + disabledModsList.Deselect(); + } + else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) + { + currentSwapFunc?.Invoke(); + } + }, + onDraw: (spriteBatch, component) => + { + enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + + void UpdateDraggingSounds() + { + if (draggedElement != null) + { + if (enabledModsList.DraggedElement == null && disabledModsList.DraggedElement == null) + { + SetDragOrigin(null); + } + CheckDragStopSound(enabledModsList); + CheckDragStopSound(disabledModsList); + } + else if (enabledModsList.DraggedElement != null) + { + SetDragOrigin(enabledModsList); + } + else if (disabledModsList.DraggedElement != null) + { + SetDragOrigin(disabledModsList); + } + + void SetDragOrigin(GUIListBox? listBox) + { + draggedElement = listBox?.DraggedElement; + draggedElementOrigin = listBox; + } + + void CheckDragStopSound(GUIListBox listBox) + { + listBox.PlaySoundOnDragStop = listBox.DraggedElement != null && draggedElementOrigin != listBox; + } + } + } + + protected override void UpdateModListItemVisibility() + { + string str = modsListFilter.Text; + enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); + } + + private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) + { + var iconBtn = guiItem.GetChild()?.GetAllChildren().Last(); + + bool matches = false; + matches |= modsListFilterTickboxes[Filter.ShowLocal].Selected + && ContentPackageManager.LocalPackages.Contains(p); + matches |= modsListFilterTickboxes[Filter.ShowPublished].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier == "WorkshopMenu.PublishedIcon"); + matches |= modsListFilterTickboxes[Filter.ShowWorkshop].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier != "WorkshopMenu.PublishedIcon"); + + if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.All(f => f is BaseSubFile || f is ItemAssemblyFile)) + { + //Both the subs-only tickbox and the item-assembly-only tickbox + //are enabled, and all files match either of them so show this mod + } + else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && p.Files.Any(f => !(f is BaseSubFile))) + { + matches = false; + } + else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.Any(f => !(f is ItemAssemblyFile))) + { + matches = false; + } + + return matches; + } + + private void PrepareToShowModInfo(ContentPackage mod) + { + TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } + if (item is null) { return; } + onInstalledInfoButtonHit(item.Value); + }); + } + + public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) + { + bulkUpdateButton.Enabled = false; + bulkUpdateButton.ToolTip = ""; + ContentPackageManager.UpdateContentPackageList(); + + SwapDropdownValues(enabledCoreDropdown, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + style: "ListBoxElement") + { + UserData = mod + }; + + var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), + onUpdate: (f, component) => + { + var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) + if (parentList is null) { return; } + if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) + { + if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } + static void noop() { } + + List contextMenuOptions = new List(); + if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + contextMenuOptions.Add( + new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); + } + + var labelConditions + = (parentList == enabledRegularModsList, parentList.AllSelected.Count > 1); + Identifier swapLabel = (labelConditions switch + { + (false, true) => "EnableSelectedWorkshopMods", + (false, false) => "EnableWorkshopMod", + (true, true) => "DisableSelectedWorkshopMods", + (true, false) => "DisableWorkshopMod" + }).ToIdentifier(); + + contextMenuOptions.Add(new ContextMenuOption(swapLabel, + isEnabled: true, onSelected: currentSwapFunc ?? noop)); + + var selectedMods = parentList.AllSelected.Select(it => it.UserData) + .OfType().ToArray(); + if (selectedMods.All(ContentPackageManager.LocalPackages.Contains) && selectedMods.Length > 1) + { + contextMenuOptions.Add(new ContextMenuOption("MergeSelectedMods".ToIdentifier(), isEnabled: true, + onSelected: () => ModMerger.AskMerge(selectedMods))); + } + + GUIButton? iconBtn(GUIComponent component) => component.GetChild()?.GetAllChildren().Last(); + if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains) + && parentList.AllSelected.All(c => iconBtn(c)?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") + && selectedMods.Length > 0) + { + contextMenuOptions.Add(new ContextMenuOption( + (selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), + isEnabled: true, + onSelected: () => + { + TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } + items.ForEach(it => + { + if (!(it is { } item)) { return; } + + item.Unsubscribe(); + SteamManager.Workshop.Uninstall(item); + PopulateInstalledModLists(); + }); + }); + })); + } + + GUIContextMenu.CreateContextMenu( + pos: PlayerInput.MousePosition, + header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), + headerColor: null, + contextMenuOptions.ToArray()); + } + }); + + var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) + { + CanBeFocused = false + }; + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), + text: mod.Name) + { + CanBeFocused = false + }; + if (mod.Errors.Any()) + { + CreateModErrorInfo(mod, modFrame, modName); + } + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.EditButton") + { + OnClicked = (button, o) => + { + ToolBox.OpenFileWithShell(mod.Dir); + return false; + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: null) + { + CanBeSelected = false, + OnClicked = (button, o) => + { + PrepareToShowModInfo(mod); + return false; + } + }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } + TaskPool.AddIfNotFound( + $"DetermineUpdateRequired{mod.SteamWorkshopId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (!isUpToDate) + { + infoButton.CanBeSelected = true; + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + } + }); + } + } + + void addRegularModsToList(IEnumerable mods, GUIListBox list) + { + list.ClearChildren(); + foreach (var mod in mods) + { + addRegularModToList(mod, list); + } + } + + var enabledMods = + (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) + ? ContentPackageManager.EnabledPackages.Regular + : enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType() + .Where(p => ContentPackageManager.RegularPackages.Contains(p))) + .ToArray(); + var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); + + addRegularModsToList(enabledMods, enabledRegularModsList); + if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } + + TaskPool.AddIfNotFound( + $"DetermineWorkshopModIcons", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + var ids = items.Select(it => it.Id).ToHashSet(); + + foreach (var child in enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children)) + { + var mod = child.UserData as RegularPackage; + if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } + + var btn = child.GetChild()?.GetAllChildren().Last(); + if (btn is null) { continue; } + if (btn.Style != null) { continue; } + + btn.ApplyStyle( + GUIStyle.GetComponentStyle( + ids.Contains(mod.SteamWorkshopId) + ? "WorkshopMenu.PublishedIcon" + : "WorkshopMenu.DownloadedIcon")); + btn.ToolTip = TextManager.Get( + ids.Contains(mod.SteamWorkshopId) + ? "PublishedWorkshopMod" + : "DownloadedWorkshopMod"); + btn.HoverCursor = CursorState.Default; + } + }); + + UpdateModListItemVisibility(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index dc9e38c57..e4e155950 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -151,7 +151,10 @@ namespace Barotrauma.Steam onDeselected: () => itemList?.Deselect(), out var select, out var deselect); - itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; itemList.RectTransform.SetAsFirstChild(); workshopItemList = itemList; @@ -415,7 +418,7 @@ namespace Barotrauma.Steam itemListBox.ClearChildren(); itemListBox.Deselect(); itemListBox.ScrollBar.BarScroll = 0.0f; - TaskPool.Add("PopulateTabWithItemList", items, + TaskPool.AddIfNotFound("PopulateTabWithItemList", items, (t) => { taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; @@ -588,10 +591,16 @@ namespace Barotrauma.Steam bool reinstallAction(GUIButton button, object o) { - TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.Reinstall(workshopItem), t => + int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage); + TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}", + SteamManager.Workshop.Reinstall(workshopItem), t => { ContentPackageManager.WorkshopPackages.Refresh(); ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) + { + mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); + } }); return false; } @@ -606,7 +615,7 @@ namespace Barotrauma.Steam if (contentPackage != null) { - TaskPool.Add( + TaskPool.AddIfNotFound( $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", contentPackage.IsUpToDate(), t => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs new file mode 100644 index 000000000..df5e42de0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -0,0 +1,256 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + readonly struct ModListPreset + { + public const string SavePath = "ModLists"; + + public enum ModType + { + Vanilla, + Local, + Workshop + } + + public readonly string Name; + public readonly CorePackage CorePackage; + public readonly ImmutableArray RegularPackages; + + public ModListPreset(XDocument doc) + { + Name = doc.Root!.GetAttributeString("name", ""); + + CorePackage corePackage = ContentPackageManager.VanillaCorePackage!; + List regularPackages = new List(); + void addPkg(ContentPackage pkg) + { + if (pkg is CorePackage core) { corePackage = core; } + else if (pkg is RegularPackage reg) { regularPackages.Add(reg); } + } + + foreach (var element in doc.Root!.Elements()) + { + ModType modType = Enum.TryParse(element.Name.LocalName, ignoreCase: true, out var mt) ? mt : ModType.Local; + + switch (modType) + { + case ModType.Vanilla: + CorePackage = ContentPackageManager.VanillaCorePackage!; + break; + case ModType.Workshop: + { + var id = element.GetAttributeUInt64("id", 0); + var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); + if (id != 0 && pkg != null) + { + addPkg(pkg); + } + } + break; + case ModType.Local: + { + var name = element.GetAttributeString("name", ""); + var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); + if (!name.IsNullOrEmpty() && pkg != null) + { + addPkg(pkg); + } + } + break; + } + } + + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public ModListPreset(string name, CorePackage corePackage, IReadOnlyList regularPackages) + { + Name = name; + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public RichString GetTooltip() + { + LocalizedString retVal = $"‖color:gui.orange‖{Name}‖end‖" //TODO: we need a RichString builder + + "\n " + TextManager.AddPunctuation(':', TextManager.Get("CorePackage")) + + "\n - " + CorePackage.Name; + if (RegularPackages.Any()) + { + retVal += "\n " + TextManager.AddPunctuation(':', TextManager.Get("RegularPackages")) + + "\n - " + + LocalizedString.Join("\n - ", RegularPackages.Select(p => (LocalizedString)p.Name)); + } + + return RichString.Rich(retVal); + } + + public void Save() + { + XDocument newDoc = new XDocument(); + XElement newRoot = new XElement("mods", new XAttribute("name", Name)); + newDoc.Add(newRoot); + + ModType determineType(ContentPackage pkg) + { + if (pkg == ContentPackageManager.VanillaCorePackage) { return ModType.Vanilla; } + if (ContentPackageManager.WorkshopPackages.Contains(pkg)) { return ModType.Workshop; } + return ModType.Local; + } + void writePkgElem(ContentPackage pkg) + { + var pkgType = determineType(pkg); + var pkgElem = new XElement(pkgType.ToString()); + switch (pkgType) + { + case ModType.Workshop: + pkgElem.SetAttributeValue("name", pkg.Name); + pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); + break; + case ModType.Local: + pkgElem.SetAttributeValue("name", pkg.Name); + break; + } + newRoot.Add(pkgElem); + } + writePkgElem(CorePackage); + RegularPackages.ForEach(writePkgElem); + + if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath); } + newDoc.SaveSafe(Path.Combine(SavePath, ToolBox.RemoveInvalidFileNameChars($"{Name}.xml"))); + } + } +} + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private bool OpenLoadPreset(GUIButton _, object __) + { + OpenLoadPreset(); + return false; + } + + private void OpenLoadPreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("LoadModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Load"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.6f)); + + var presetListBox = new GUIListBox(new RectTransform((1.0f, 0.7f), msgBox.Content.RectTransform)); + + (string Path, XDocument? Doc) tryLoadXml(string path) + => (path, XMLExtensions.TryLoadXml(path)); + + var presets = Directory.Exists(ModListPreset.SavePath) + ? Directory.GetFiles(ModListPreset.SavePath) + .Select(tryLoadXml) + .Where(d => d.Doc != null) + .ToArray() + : Array.Empty<(string Path, XDocument? Doc)>(); + + foreach (var doc in presets) + { + ModListPreset preset = new ModListPreset(doc.Doc!); + var presetFrame = new GUIFrame(new RectTransform((1.0f, 0.09f), presetListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = preset, + ToolTip = preset.GetTooltip() + }; + new GUITextBlock(new RectTransform(Vector2.One, presetFrame.RectTransform), preset.Name) + { + CanBeFocused = false + }; + var deleteBtn + = new GUIButton(new RectTransform((0.2f, 1.0f), presetFrame.RectTransform, Anchor.CenterRight), + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + File.Delete(doc.Path); + presetListBox.Content.RemoveChild(presetFrame); + return false; + } + }; + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (presetListBox.SelectedData is ModListPreset preset) + { + var allChildren = enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children) + .ToArray(); + enabledRegularModsList.ClearChildren(); + disabledRegularModsList.ClearChildren(); + var toEnable = + allChildren.Where(c => c.UserData is RegularPackage p + && preset.RegularPackages.Contains(p)) + .OrderBy(c => c.UserData is RegularPackage p ? preset.RegularPackages.IndexOf(p) : int.MaxValue) + .ToArray(); + var toDisable = allChildren.Where(c => !toEnable.Contains(c)).ToArray(); + toEnable.ForEach(c => c.RectTransform.Parent = enabledRegularModsList.Content.RectTransform); + toDisable.ForEach(c => c.RectTransform.Parent = disabledRegularModsList.Content.RectTransform); + + enabledCoreDropdown.SelectItem(preset.CorePackage); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + private bool OpenSavePreset(GUIButton _, object __) + { + OpenSavePreset(); + return false; + } + + private void OpenSavePreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("SaveModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Save"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.2f)); + + var nameBox = new GUITextBox(new RectTransform((1.0f, 0.3f), msgBox.Content.RectTransform), ""); + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (nameBox.Text.IsNullOrEmpty()) + { + nameBox.Flash(GUIStyle.Red); + return false; + } + + if (enabledCoreDropdown.SelectedData is CorePackage corePackage) + { + ModListPreset preset = new ModListPreset(nameBox.Text, + corePackage, + enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType().ToArray()); + preset.Save(); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 49632af2b..c8563eb3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -3,9 +3,9 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; +using System.Threading.Tasks; using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam @@ -20,20 +20,20 @@ namespace Barotrauma.Steam Publish } + private enum Filter + { + ShowLocal, + ShowWorkshop, + ShowPublished, + ShowOnlySubs, + ShowOnlyItemAssemblies + } + private readonly GUILayoutGroup tabber; private readonly Dictionary tabContents; private readonly GUIFrame contentFrame; - private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); - - private readonly GUIDropDown enabledCoreDropdown; - private readonly GUIListBox enabledRegularModsList; - private readonly GUIListBox disabledRegularModsList; - private readonly Action onInstalledInfoButtonHit; - private readonly GUITextBox modsListFilter; - private readonly GUIButton bulkUpdateButton; - private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); @@ -41,7 +41,7 @@ namespace Barotrauma.Steam private readonly GUIListBox selfModsList; private uint memSubscribedModCount = 0; - + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -62,6 +62,7 @@ namespace Barotrauma.Steam out disabledRegularModsList, out onInstalledInfoButtonHit, out modsListFilter, + out modsListFilterTickboxes, out bulkUpdateButton); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -69,45 +70,6 @@ namespace Barotrauma.Steam SelectTab(Tab.InstalledMods); } - private void UpdateSubscribedModInstalls() - { - if (!SteamManager.IsInitialized) { return; } - - uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); - if (numSubscribedMods == memSubscribedModCount) { return; } - memSubscribedModCount = numSubscribedMods; - - var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); - var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); - foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(item); - } - } - - TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => - { - if (!t.TryGetResult(out ISet publishedItems)) { return; } - - var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); - bool needsRefresh = false; - foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - SteamManager.Workshop.Uninstall(item); - needsRefresh = true; - } - - if (needsRefresh) - { - PopulateInstalledModLists(); - } - }); - } - private void SwitchContent(GUIFrame newContent) { contentFrame.Children.ForEach(c => c.Visible = false); @@ -161,460 +123,6 @@ namespace Barotrauma.Steam return content; } - private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( - GUIComponent parent, - float leftWidth = 0.3875f, - float centerWidth = 0.025f, - float rightWidth = 0.5875f, - bool split = false, - float height = 1.0f) - { - GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); - GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); - var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); - if (split) - { - new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), - onDraw: (sb, c) => - { - sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); - }); - } - GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); - return (left, center, right); - } - - private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) - { - if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) - { - //move the dragged elements to the index determined previously - var draggedElement = from.DraggedElement; - - var selected = from.AllSelected.ToList(); - selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); - - float oldCount = to.Content.CountChildren; - float newCount = oldCount + selected.Count; - - var offset = draggedElement.RectTransform.AbsoluteOffset; - offset += from.Content.Rect.Location; - offset -= to.Content.Rect.Location; - - for (int i = 0; i < selected.Count; i++) - { - var c = selected[i]; - c.Parent.RemoveChild(c); - c.RectTransform.Parent = to.Content.RectTransform; - c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); - } - - from.DraggedElement = null; - from.Deselect(); - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - - //recalculate the dragged element's offset so it doesn't jump around - draggedElement.RectTransform.AbsoluteOffset = offset; - - to.DraggedElement = draggedElement; - - to.BarScroll *= (oldCount / newCount); - } - } - - private Action? currentSwapFunc = null; - - private void SetSwapFunc(GUIListBox from, GUIListBox to) - { - currentSwapFunc = () => - { - to.Deselect(); - var selected = from.AllSelected.ToArray(); - foreach (var frame in selected) - { - frame.Parent.RemoveChild(frame); - frame.RectTransform.Parent = to.Content.RectTransform; - } - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - }; - } - - private void CreateInstalledModsTab( - out GUIDropDown enabledCoreDropdown, - out GUIListBox enabledRegularModsList, - out GUIListBox disabledRegularModsList, - out Action onInstalledInfoButtonHit, - out GUITextBox modsListFilter, - out GUIButton bulkUpdateButton) - { - GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); - - CreateWorkshopItemDetailContainer( - content, - out var outerContainer, - onSelected: (itemOrPackage, selectedFrame) => - { - if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } - }, - onDeselected: () => PopulateInstalledModLists(), - out onInstalledInfoButtonHit, out var deselect); - - GUILayoutGroup mainLayout = - new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); - mainLayout.RectTransform.SetAsFirstChild(); - - var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); - topLeft.Stretch = true; - Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); - enabledCoreDropdown = Dropdown(topLeft, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }, - heightScale: 1.0f / 13.0f); - Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); - topRight.ChildAnchor = Anchor.CenterLeft; - - var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - void padTopRight(float width=1.0f) - { - new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); - } - - padTopRight(); - //TODO: put stuff here - padTopRight(width: 3.0f); - var refreshListsButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIReloadButton") - { - OnClicked = (b, o) => - { - PopulateInstalledModLists(); - return false; - }, - ToolTip = TextManager.Get("RefreshModLists") - }; - bulkUpdateButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIUpdateButton") - { - OnClicked = (b, o) => - { - BulkDownloader.PrepareUpdates(); - return false; - }, - Enabled = false - }; - padTopRight(width: 0.1f); - - var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); - right.ChildAnchor = Anchor.TopRight; - - //enabled mods - Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); - var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - enabledRegularModsList = enabledModsList; - - //disabled mods - Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); - var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - disabledRegularModsList = disabledModsList; - - var centerButton = - new GUIButton( - new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, - anchor: Anchor.Center), - style: "GUIButtonToggleLeft") - { - Visible = false, - OnClicked = (button, o) => - { - currentSwapFunc?.Invoke(); - return false; - } - }; - - enabledModsList.OnSelected = (frame, o) => - { - disabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); - - SetSwapFunc(enabledModsList, disabledModsList); - - return true; - }; - disabledModsList.OnSelected = (frame, o) => - { - enabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); - - SetSwapFunc(disabledModsList, enabledModsList); - - return true; - }; - - var searchBox = CreateSearchBox(mainLayout, width: 0.5f); - modsListFilter = searchBox; - - new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), - onUpdate: (f, component) => - { - HandleDraggingAcrossModLists(enabledModsList, disabledModsList); - HandleDraggingAcrossModLists(disabledModsList, enabledModsList); - if (PlayerInput.PrimaryMouseButtonClicked() - && !GUI.IsMouseOn(enabledModsList) - && !GUI.IsMouseOn(disabledModsList) - && GUIContextMenu.CurrentContextMenu is null) - { - enabledModsList.Deselect(); - disabledModsList.Deselect(); - } - else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) - { - currentSwapFunc?.Invoke(); - } - }, - onDraw: (spriteBatch, component) => - { - enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - }); - } - - protected override void UpdateModListItemVisibility() - { - string str = modsListFilter.Text; - enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); - } - - private void PrepareToShowModInfo(ContentPackage mod) - { - TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), - t => - { - if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } - if (item is null) { return; } - onInstalledInfoButtonHit(item.Value); - }); - } - - public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) - { - bulkUpdateButton.Enabled = false; - bulkUpdateButton.ToolTip = ""; - ContentPackageManager.UpdateContentPackageList(); - - SwapDropdownValues(enabledCoreDropdown, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }); - - void addRegularModToList(RegularPackage mod, GUIListBox list) - { - var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), - style: "ListBoxElement") - { - UserData = mod - }; - - var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), - onUpdate: (f, component) => - { - var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) - if (parentList is null) { return; } - if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) - { - if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } - static void noop() { } - - List contextMenuOptions = new List(); - if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - contextMenuOptions.Add( - new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); - } - - Identifier swapLabel - = ((parentList == enabledRegularModsList ? "Disable" : "Enable") - + (parentList.AllSelected.Count > 1 ? "SelectedWorkshopMods" : "WorkshopMod")) - .ToIdentifier(); - - contextMenuOptions.Add(new ContextMenuOption(swapLabel, - isEnabled: true, onSelected: currentSwapFunc ?? noop)); - - GUIContextMenu.CreateContextMenu( - pos: PlayerInput.MousePosition, - header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), - headerColor: null, - contextMenuOptions.ToArray()); - } - }); - - var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), - style: "GUIDragIndicator") - { - CanBeFocused = false - }; - - var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) - { - CanBeFocused = false - }; - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), - text: mod.Name) - { - CanBeFocused = false - }; - if (mod.Errors.Any()) - { - CreateModErrorInfo(mod, modFrame, modName); - } - if (ContentPackageManager.LocalPackages.Contains(mod)) - { - var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: "WorkshopMenu.EditButton") - { - OnClicked = (button, o) => - { - ToolBox.OpenFileWithShell(mod.Dir); - return false; - }, - ToolTip = TextManager.Get("OpenLocalModInExplorer") - }; - } - else if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - var infoButton = new GUIButton( - new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: null) - { - CanBeSelected = false, - OnClicked = (button, o) => - { - PrepareToShowModInfo(mod); - return false; - } - }; - if (!SteamManager.IsInitialized) - { - infoButton.Enabled = false; - } - TaskPool.Add( - $"DetermineUpdateRequired{mod.SteamWorkshopId}", - mod.IsUpToDate(), - t => - { - if (!t.TryGetResult(out bool isUpToDate)) { return; } - - if (!isUpToDate) - { - infoButton.CanBeSelected = true; - infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); - infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); - bulkUpdateButton.Enabled = true; - bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); - } - }); - } - } - - void addRegularModsToList(IEnumerable mods, GUIListBox list) - { - list.ClearChildren(); - foreach (var mod in mods) - { - addRegularModToList(mod, list); - } - } - - var enabledMods = - (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) - ? ContentPackageManager.EnabledPackages.Regular - : enabledRegularModsList.Content.Children - .Select(c => c.UserData) - .OfType() - .Where(p => ContentPackageManager.RegularPackages.Contains(p))) - .ToArray(); - var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); - - addRegularModsToList(enabledMods, enabledRegularModsList); - if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } - - TaskPool.Add( - $"DetermineWorkshopModIcons", - SteamManager.Workshop.GetPublishedItems(), - t => - { - if (!t.TryGetResult(out ISet items)) { return; } - var ids = items.Select(it => it.Id).ToHashSet(); - - foreach (var child in enabledRegularModsList.Content.Children - .Concat(disabledRegularModsList.Content.Children)) - { - var mod = child.UserData as RegularPackage; - if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } - - var btn = child.GetChild()?.GetAllChildren().Last(); - if (btn is null) { continue; } - if (btn.Style != null) { continue; } - - btn.ApplyStyle( - GUIStyle.GetComponentStyle( - ids.Contains(mod.SteamWorkshopId) - ? "WorkshopMenu.PublishedIcon" - : "WorkshopMenu.DownloadedIcon")); - btn.ToolTip = TextManager.Get( - ids.Contains(mod.SteamWorkshopId) - ? "PublishedWorkshopMod" - : "DownloadedWorkshopMod"); - btn.HoverCursor = CursorState.Default; - } - }); - - UpdateModListItemVisibility(); - } - private void CreatePopularModsTab(out GUIListBox popularModsList) { GUIFrame content = CreateNewContentFrame(Tab.PopularMods); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index ec55b9e72..136290f81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -44,12 +44,7 @@ namespace Barotrauma.Steam } }); } - - ~LocalThumbnail() - { - Dispose(); - } - + private bool disposed = false; public void Dispose() { @@ -197,6 +192,11 @@ namespace Barotrauma.Steam FileSelection.OnFileSelected = (fn) => { + if (new FileInfo(fn).Length > SteamManager.Workshop.MaxThumbnailSize) + { + new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); + return; + } thumbnailPath = fn; CreateLocalThumbnail(thumbnailPath, thumbnailContainer); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index 692bc61b9..cbe5fca4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -106,10 +106,8 @@ namespace Barotrauma.Steam => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) { UserData = new ActionCarrier(id, action) }; - protected GUITextBox CreateSearchBox(GUILayoutGroup mainLayout, float width = 1.0f, float heightScale = 1.0f) + protected GUITextBox CreateSearchBox(RectTransform searchRectT) { - var searchRectT = NewItemRectT(mainLayout, heightScale: heightScale); - searchRectT.RelativeSize = (width, searchRectT.RelativeSize.Y); var searchHolder = new GUIFrame(searchRectT, style: null); var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, @@ -119,6 +117,11 @@ namespace Barotrauma.Steam { CanBeFocused = false }; + new GUICustomComponent(new RectTransform(Vector2.Zero, searchHolder.RectTransform), onUpdate: + (f, component) => + { + searchTitle.RectTransform.NonScaledSize = searchBox.Frame.RectTransform.NonScaledSize; + }); searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; @@ -137,7 +140,8 @@ namespace Barotrauma.Steam const int maxErrorsToShow = 5; nameText.TextColor = GUIStyle.Red; uiElement.ToolTip = - TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.error)); + TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); if (mod.Errors.Count() > maxErrorsToShow) { uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs index ebe59aaf8..003aab946 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs @@ -1,3 +1,5 @@ +using System; + #nullable enable namespace Barotrauma.Steam @@ -7,5 +9,8 @@ namespace Barotrauma.Steam public WorkshopMenu(GUIFrame parent) { } protected abstract void UpdateModListItemVisibility(); + + protected bool ModNameMatches(ContentPackage p, string query) + => p.Name.Contains(query, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs index 8e652c5cd..804c688ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs @@ -8,7 +8,6 @@ namespace Barotrauma private readonly int maxWidth; private ScalableFont? cachedFont = null; - private uint cachedFontSize = 0; public LimitLString(LocalizedString text, GUIFont font, int maxWidth) { @@ -27,7 +26,6 @@ namespace Barotrauma { cachedValue = ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth); cachedFont = font.Value; - cachedFontSize = font.Size; UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 397c71876..c11566a30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -404,7 +404,7 @@ namespace Barotrauma public static string LimitString(string str, ScalableFont font, int maxWidth) { - if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) return ""; + if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) { return ""; } float currWidth = font.MeasureString("...").X; for (int i = 0; i < str.Length; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 3dade74cc..272f9e4c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -76,7 +76,7 @@ namespace Barotrauma float zoom = (float)texWidth / (float)boundingBox.Width; int texHeight = (int)(zoom * boundingBox.Height); - Camera cam = new Camera(); + using Camera cam = new Camera(); cam.SetResolution(new Point(texWidth, texHeight)); cam.MaxZoom = zoom; cam.MinZoom = zoom * 0.5f; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index d6ed8ba85..1f9b0f11c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -64,11 +64,18 @@ true - + + + + + + + + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 2185d1a08..630273d43 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -55,19 +55,20 @@ true - + + + + + + + + - - SharedSource\Prefabs\PrefabSelector.cs - - - SharedSource\Prefabs\PrefabCollectionSubset.cs - diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 95e6f1d27..26662833d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -61,19 +61,20 @@ true - + + + + + + + - - SharedSource\Steam\AuthTicket.cs - - - SharedSource\Utils\Result.cs - + - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index a9f7c03f7..edd4b10ac 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -65,10 +65,11 @@ - + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 629e28b76..d0e34c416 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -58,10 +58,11 @@ - + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a63d4e033..197c22a26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -10,6 +10,11 @@ namespace Barotrauma { private readonly Dictionary prevSentSkill = new Dictionary(); + /// + /// The client opted to create a new character and discard this one + /// + public bool Discarded; + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } @@ -37,7 +42,7 @@ namespace Barotrauma partial void OnPermanentStatChanged(StatTypes statType) { if (Character == null || Character.Removed) { return; } - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData(statType)); } public void ServerWrite(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c58fb75c8..9ad728290 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -647,6 +647,7 @@ namespace Barotrauma { msg.Write(false); } + msg.Write(HumanPrefabHealthMultiplier); msg.Write(Wallet.Balance); msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); msg.Write((byte)TeamID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 61c141015..a9a05313a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1676,7 +1676,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; } - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; NewMessage(client.Name + (GameMain.GameSession.Map.AllowDebugTeleport ? " enabled" : " disabled") + " teleportation on the campaign map.", Color.White); GameMain.Server.SendConsoleMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", client); @@ -2274,7 +2274,6 @@ namespace Barotrauma Wallet wallet = targetCharacter is null ? campaign.Bank : targetCharacter.Wallet; wallet.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); - campaign.LastUpdateID++; } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 541435d72..2c7358b04 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -25,9 +25,9 @@ namespace Barotrauma /// /// Saves bots in multiplayer /// - public void SaveMultiplayer(XElement root) + public XElement SaveMultiplayer(XElement parentElement) { - XElement saveElement = new XElement("bots", new XAttribute("hasbots", HasBots)); + var element = new XElement("bots", new XAttribute("hasbots", HasBots)); foreach (CharacterInfo info in characterInfos) { if (Level.Loaded != null) @@ -35,13 +35,13 @@ namespace Barotrauma if (!info.IsNewHire && (info.Character == null || info.Character.IsDead)) { continue; } } - XElement characterElement = info.Save(saveElement); + XElement characterElement = info.Save(element); if (info.InventoryData != null) { characterElement.Add(info.InventoryData); } if (info.HealthData != null) { characterElement.Add(info.HealthData); } if (info.OrderData != null) { characterElement.Add(info.OrderData); } } - SaveActiveOrders(saveElement); - root.Add(saveElement); + parentElement?.Add(element); + return element; } public void ServerWriteActiveOrders(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index a2bfc7450..9360c71d2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -10,6 +11,31 @@ namespace Barotrauma protected set; } + private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; + + /// + /// There is a client-side implementation of the method in + /// + public bool AllowedToManageCampaign(Client client, ClientPermissions permissions) + { + //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, + //or if no-one has management permissions + return + client.HasPermission(permissions) || + client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Server.ConnectedClients.Count == 1 || + IsOwner(client) || + GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); + } + + public bool AllowedToManageWallets(Client client) + { + return + client.HasPermission(ClientPermissions.ManageCampaign) || + client.HasPermission(ClientPermissions.ManageMoney) || + IsOwner(client); + } + public override void ShowStartMessage() { foreach (Mission mission in Missions) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 965530ee9..ae5c4e510 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -37,7 +37,7 @@ namespace Barotrauma { if (forceMapUI == value) { return; } forceMapUI = value; - LastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); } } @@ -71,11 +71,43 @@ namespace Barotrauma get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } } - public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings settings) + private bool purchasedHullRepairs, purchasedLostShuttles, purchasedItemRepairs; + public override bool PurchasedHullRepairs + { + get { return purchasedHullRepairs; } + set + { + if (purchasedHullRepairs == value) { return; } + purchasedHullRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedLostShuttles + { + get { return purchasedLostShuttles; } + set + { + if (purchasedLostShuttles == value) { return; } + purchasedLostShuttles = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedItemRepairs + { + get { return purchasedItemRepairs; } + set + { + if (purchasedItemRepairs == value) { return; } + purchasedItemRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + + public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings startingSettings) { if (string.IsNullOrWhiteSpace(savePath)) { return; } - GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, settings, seed); + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, startingSettings, seed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); @@ -158,34 +190,11 @@ namespace Barotrauma public override void Start() { base.Start(); - lastUpdateID++; + IncrementAllLastUpdateIds(); } private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; - /// - /// There is a client-side implementation of the method in - /// - public bool AllowedToManageCampaign(Client client, ClientPermissions permissions) - { - //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, - //or if no-one has management permissions - return - client.HasPermission(permissions) || - client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Server.ConnectedClients.Count == 1 || - IsOwner(client) || - GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); - } - - public bool AllowedToManageWallets(Client client) - { - return - client.HasPermission(ClientPermissions.ManageCampaign) || - client.HasPermission(ClientPermissions.ManageMoney) || - IsOwner(client); - } - public void SaveExperiencePoints(Client client) { ClearSavedExperiencePoints(client); @@ -200,14 +209,6 @@ namespace Barotrauma savedExperiencePoints.RemoveAll(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint)); } - public void LoadPets() - { - if (petsElement != null) - { - PetBehavior.LoadPets(petsElement); - } - } - public void SavePlayers() { //refresh the character data of clients who are still in the server @@ -229,7 +230,7 @@ namespace Barotrauma if (!matchingCharacterData.HasSpawned) { continue; } characterInfo ??= matchingCharacterData.CharacterInfo; } - if (characterInfo == null) { continue; } + if (characterInfo == null || characterInfo.Discarded) { continue; } //reduce skills if the character has died if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { @@ -261,8 +262,7 @@ namespace Barotrauma characterData.ForEach(cd => cd.HasSpawned = false); - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); + SavePets(); //remove all items that are in someone's inventory foreach (Character c in Character.CharacterList) @@ -285,6 +285,8 @@ namespace Barotrauma c.Inventory.DeleteAllItems(); } + + SaveActiveOrders(); } public void MoveDiscardedCharacterBalancesToBank() @@ -304,7 +306,7 @@ namespace Barotrauma protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { - lastUpdateID++; + IncrementAllLastUpdateIds(); switch (transitionType) { @@ -348,44 +350,11 @@ namespace Barotrauma if (success) { SavePlayers(); - yield return CoroutineStatus.Running; - - if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) - { - Submarine.MainSub = leavingSub; - GameMain.GameSession.Submarine = leavingSub; - GameMain.GameSession.SubmarineInfo = leavingSub.Info; - leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); - var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); - foreach (Submarine sub in subsToLeaveBehind) - { - GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); - LinkedSubmarine.CreateDummy(leavingSub, sub); - } - } + LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - - if (PendingSubmarineSwitch != null) - { - SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - - for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) - { - GameMain.GameSession.OwnedSubmarines[i] = previousSub; - break; - } - } - } - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - PendingSubmarineSwitch = null; } else { @@ -393,7 +362,7 @@ namespace Barotrauma GameMain.Server.EndGame(TransitionType.None, wasSaved: false); LoadCampaign(GameMain.GameSession.SavePath); LastSaveID++; - LastUpdateID++; + IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; } @@ -424,14 +393,14 @@ namespace Barotrauma } partial void InitProjSpecific() - { - CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; }; - CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; }; - CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; - UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; - Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; - Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; - Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; + { + CargoManager.OnItemsInBuyCrateChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate); }; + CargoManager.OnPurchasedItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.PurchasedItems); }; + CargoManager.OnSoldItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.SoldItems); }; + UpgradeManager.OnUpgradesChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager); }; + Map.OnLocationSelected += (loc, connection) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Map.OnMissionsSelected += (loc, mission) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Reputation.OnAnyReputationValueChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.Reputation); }; //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; @@ -451,7 +420,10 @@ namespace Barotrauma { discardedCharacters.Add(data); } + DebugConsole.Log($"Client \"{client}\" discarded the character ({data.Name})"); + data.CharacterInfo.Discarded = true; characterData.Remove(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } } } @@ -466,6 +438,7 @@ namespace Barotrauma characterData.RemoveAll(cd => cd.MatchesClient(client)); var data = new CharacterCampaignData(client); characterData.Add(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); return data; } @@ -477,6 +450,7 @@ namespace Barotrauma var matchingData = GetClientCharacterData(client); if (matchingData != null) { client.CharacterInfo = matchingData.CharacterInfo; } } + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } public Dictionary GetAssignedJobs(IEnumerable connectedClients) @@ -581,127 +555,187 @@ namespace Barotrauma base.End(transitionType); } + private bool IsFlagRequired(Client c, NetFlags flag) + => !c.LastRecvCampaignUpdate.TryGetValue(flag, out var id) || NetIdUtils.IdMoreRecent(GetLastUpdateIdForFlag(flag), id); + public void ServerWrite(IWriteMessage msg, Client c) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); - Reputation reputation = Map?.CurrentLocation?.Reputation; + NetFlags requiredFlags = lastUpdateID.Keys.Where(k => IsFlagRequired(c, k)).Aggregate((NetFlags)0, (f1, f2) => f1 | f2); + + msg.Write((UInt16)requiredFlags); msg.Write(IsFirstRound); msg.Write(CampaignID); - msg.Write(lastUpdateID); msg.Write(lastSaveID); msg.Write(map.Seed); - msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); - - var selectedMissionIndices = map.GetSelectedMissionIndices(); - msg.Write((byte)selectedMissionIndices.Count()); - foreach (int selectedMissionIndex in selectedMissionIndices) + + if (requiredFlags.HasFlag(NetFlags.Misc)) { - msg.Write((byte)selectedMissionIndex); + msg.Write(GetLastUpdateIdForFlag(NetFlags.Misc)); + msg.Write(PurchasedHullRepairs); + msg.Write(PurchasedItemRepairs); + msg.Write(PurchasedLostShuttles); } - var subList = GameMain.NetLobbyScreen.GetSubList(); - List ownedSubmarineIndices = new List(); - for (int i = 0; i < subList.Count; i++) + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) { - if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) + msg.Write(GetLastUpdateIdForFlag(NetFlags.MapAndMissions)); + msg.Write(ForceMapUI); + msg.Write(map.AllowDebugTeleport); + msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); + msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + + if (map.CurrentLocation != null) { - ownedSubmarineIndices.Add(i); - } - } - msg.Write((ushort)ownedSubmarineIndices.Count); - foreach (int index in ownedSubmarineIndices) - { - msg.Write((ushort)index); - } - - msg.Write(map.AllowDebugTeleport); - msg.Write(reputation != null); - if (reputation != null) { msg.Write(reputation.Value); } - - // hopefully we'll never have more than 128 factions - msg.Write((byte)Factions.Count); - foreach (Faction faction in Factions) - { - msg.Write(faction.Prefab.Identifier); - msg.Write(faction.Reputation.Value); - } - - msg.Write(ForceMapUI); - - msg.Write(PurchasedHullRepairs); - msg.Write(PurchasedItemRepairs); - msg.Write(PurchasedLostShuttles); - - if (map.CurrentLocation != null) - { - msg.Write((byte)map.CurrentLocation?.AvailableMissions.Count()); - foreach (Mission mission in map.CurrentLocation.AvailableMissions) - { - msg.Write(mission.Prefab.Identifier); - if (mission.Locations[0] == mission.Locations[1]) + msg.Write((byte)map.CurrentLocation.AvailableMissions.Count()); + foreach (Mission mission in map.CurrentLocation.AvailableMissions) { - msg.Write((byte)255); - } - else - { - Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; - LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + msg.Write(mission.Prefab.Identifier); + if (mission.Locations[0] == mission.Locations[1]) + { + msg.Write((byte)255); + } + else + { + Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; + LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); + msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + } } } - - // Store balance - bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); - msg.Write(hasStores); - if (hasStores) + else { - msg.Write((byte)map.CurrentLocation.Stores.Count); - foreach (var store in map.CurrentLocation.Stores.Values) + msg.Write((byte)0); + } + + var selectedMissionIndices = map.GetSelectedMissionIndices(); + msg.Write((byte)selectedMissionIndices.Count()); + foreach (int selectedMissionIndex in selectedMissionIndices) + { + msg.Write((byte)selectedMissionIndex); + } + + WriteStores(msg); + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.SubList)); + var subList = GameMain.NetLobbyScreen.GetSubList(); + List ownedSubmarineIndices = new List(); + for (int i = 0; i < subList.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) { - msg.Write(store.Identifier); - msg.Write((UInt16)store.Balance); + ownedSubmarineIndices.Add(i); } } + msg.Write((ushort)ownedSubmarineIndices.Count); + foreach (int index in ownedSubmarineIndices) + { + msg.Write((ushort)index); + } } - else + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) { - msg.Write((byte)0); - // Store balance - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.UpgradeManager)); + msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); + foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + { + msg.Write(prefab.Identifier); + msg.Write(category.Identifier); + msg.Write((byte)level); + } + msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + { + msg.Write(itemSwap.ItemToRemove.ID); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + } } - WriteItems(msg, CargoManager.ItemsInBuyCrate); - WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); - WriteItems(msg, CargoManager.PurchasedItems); - WriteItems(msg, CargoManager.SoldItems); - - msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); - foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) { - msg.Write(prefab.Identifier); - msg.Write(category.Identifier); - msg.Write((byte)level); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate)); + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteStores(msg); } - msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); - foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) { - msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate)); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteStores(msg); } - var characterData = GetClientCharacterData(c); - if (characterData?.CharacterInfo == null) + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) { - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.PurchasedItems)); + WriteItems(msg, CargoManager.PurchasedItems); + WriteStores(msg); + } - else + if (requiredFlags.HasFlag(NetFlags.SoldItems)) { - msg.Write(true); - characterData.CharacterInfo.ServerWrite(msg); + msg.Write(GetLastUpdateIdForFlag(NetFlags.SoldItems)); + WriteItems(msg, CargoManager.SoldItems); + WriteStores(msg); + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.Reputation)); + Reputation reputation = Map?.CurrentLocation?.Reputation; + msg.Write(reputation != null); + if (reputation != null) { msg.Write(reputation.Value); } + + // hopefully we'll never have more than 128 factions + msg.Write((byte)Factions.Count); + foreach (Faction faction in Factions) + { + msg.Write(faction.Prefab.Identifier); + msg.Write(faction.Reputation.Value); + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.CharacterInfo)); + var characterData = GetClientCharacterData(c); + if (characterData?.CharacterInfo == null) + { + msg.Write(false); + } + else + { + msg.Write(true); + characterData.CharacterInfo.ServerWrite(msg); + } + } + + void WriteStores(IWriteMessage msg) + { + if (map.CurrentLocation != null) + { + // Store balance + bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); + msg.Write(hasStores); + if (hasStores) + { + msg.Write((byte)map.CurrentLocation.Stores.Count); + foreach (var store in map.CurrentLocation.Stores.Values) + { + msg.Write(store.Identifier); + msg.Write((UInt16)store.Balance); + } + } + } + else + { + msg.Write((byte)0); + // Store balance + msg.Write(false); + } } } @@ -977,6 +1011,8 @@ namespace Barotrauma { NetWalletTransfer transfer = INetSerializableStruct.Read(msg); + if (GameMain.Server is null) { return; } + switch (transfer.Sender) { case Some { Value: var id }: @@ -992,7 +1028,8 @@ namespace Barotrauma { if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) { - GameMain.Server?.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } + GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } return; @@ -1301,7 +1338,11 @@ namespace Barotrauma } // save bots - CrewManager.SaveMultiplayer(modeElement); + var crewManagerElement = CrewManager.SaveMultiplayer(modeElement); + if (ActiveOrdersElement != null) + { + crewManagerElement.Add(ActiveOrdersElement); + } XElement savedExperiencePointsElement = new XElement("SavedExperiencePoints"); foreach (var savedExperiencePoint in savedExperiencePoints) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs index dea506acf..3ac8dd6a9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; @@ -18,7 +19,8 @@ namespace Barotrauma IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte) ServerPacketHeader.READY_CHECK); msg.Write((byte) ReadyCheckState.Start); - msg.Write(endTime); + msg.Write(new DateTimeOffset(startTime).ToUnixTimeSeconds()); + msg.Write(new DateTimeOffset(endTime).ToUnixTimeSeconds()); msg.Write(author); if (sender != null) @@ -53,10 +55,9 @@ namespace Barotrauma foreach (Client client in ActivePlayers) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ServerPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.Update); - msg.Write(time); // sync time - msg.Write((byte) state); + msg.Write((byte)ServerPacketHeader.READY_CHECK); + msg.Write((byte)ReadyCheckState.Update); + msg.Write((byte)state); msg.Write(otherClient); GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index aa7abb66a..607149e26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -1,19 +1,27 @@ using Barotrauma.Networking; -using System; namespace Barotrauma.Items.Components { - partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable + partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(docked); - if (docked) { msg.Write(DockingTarget.item.ID); msg.Write(IsLocked); } } + public void ServerEventRead(IReadMessage msg, Client c) + { + var allowOutpostAutoDocking = (AllowOutpostAutoDocking)msg.ReadByte(); + if (outpostAutoDockingPromptShown && + (GameMain.GameSession?.Campaign?.AllowedToManageCampaign(c, ClientPermissions.ManageMap) ?? false)) + { + this.allowOutpostAutoDocking = allowOutpostAutoDocking; + } + } + } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 063f23d7a..ad7dd525f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { //do nothing } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index f2249351b..125557284 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -1,13 +1,22 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; -using System.Globalization; -using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { + const float NetworkUpdateInterval = 5.0f; + private float networkUpdateTimer; + + partial void UpdateProjSpecific(float deltaTime) + { + networkUpdateTimer -= deltaTime; + if (networkUpdateTimer <= 0.0f) + { + item.CreateServerEvent(this); + networkUpdateTimer = NetworkUpdateInterval; + } + } + public void ServerEventRead(IReadMessage msg, Client c) { float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index d7223e2c8..4145c0b35 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -102,6 +102,7 @@ namespace Barotrauma.Items.Components { msg.Write(autoPilot); msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); + msg.Write(user?.ID ?? Entity.NullEntityID); if (!autoPilot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 2e27864a2..a9cffe9ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < Connections.Count; i++) { wires[i] = new List(); - for (int j = 0; j < Connections[i].MaxWires; j++) + uint wireCount = msg.ReadVariableUInt32(); + for (int j = 0; j < wireCount; j++) { ushort wireId = msg.ReadUInt16(); @@ -91,12 +92,8 @@ namespace Barotrauma.Items.Components //go through existing wire links for (int i = 0; i < Connections.Count; i++) { - int j = -1; - foreach (Wire existingWire in Connections[i].Wires) + foreach (Wire existingWire in Connections[i].Wires.ToArray()) { - j++; - if (existingWire == null) { continue; } - //existing wire not in the list of new wires -> disconnect it if (!wires[i].Contains(existingWire)) { @@ -163,7 +160,7 @@ namespace Barotrauma.Items.Components }*/ } - Connections[i].SetWire(j, null); + Connections[i].DisconnectWire(existingWire); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 7c2bc102a..c1742cfa7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -26,26 +26,34 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + if (!element.IsNumberInput) { - TextChanged(customInterfaceElementList[i], elementValues[i]); + TextChanged(element, elementValues[i]); } else { - int.TryParse(elementValues[i], out int value); - ValueChanged(customInterfaceElementList[i], value); + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(elementValues[i], out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(elementValues[i], out float value): + ValueChanged(element, value); + break; + } } } - else if (customInterfaceElementList[i].ContinuousSignal) + else if (element.ContinuousSignal) { - TickBoxToggled(customInterfaceElementList[i], elementStates[i]); + TickBoxToggled(element, elementStates[i]); } else if (elementStates[i]) { - clickedButton = customInterfaceElementList[i]; - ButtonClicked(customInterfaceElementList[i]); + clickedButton = element; + ButtonClicked(element); } } } @@ -61,13 +69,14 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - msg.Write(customInterfaceElementList[i].Signal); + msg.Write(element.Signal); } - else if(customInterfaceElementList[i].ContinuousSignal) + else if(element.ContinuousSignal) { - msg.Write(customInterfaceElementList[i].State); + msg.Write(element.State); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 9645311ce..877d0b4b1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -42,7 +42,7 @@ namespace Barotrauma.MapCreatures.Behavior foreach (BallastFloraBranch branch in Branches) { //don't notify about minuscule amounts of damage (<= 1.0f) - if (branch.AccumulatedDamage > 1.0f) + if (Math.Abs(branch.AccumulatedDamage) > 1.0f) { CreateNetworkMessage(new BranchDamageEventData(branch)); branch.AccumulatedDamage = 0.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 024b68518..9efc0c125 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -2,10 +2,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -39,9 +36,9 @@ namespace Barotrauma return; } - statusUpdateTimer -= deltaTime; - decalUpdateTimer -= deltaTime; - backgroundSectionUpdateTimer -= deltaTime; + statusUpdateTimer += deltaTime; + decalUpdateTimer += deltaTime; + backgroundSectionUpdateTimer += deltaTime; //update client hulls if the amount of water has changed by >10% //or if oxygen percentage has changed by 5% @@ -49,33 +46,32 @@ namespace Barotrauma (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || lastSentFireCount != FireSources.Count) - && statusUpdateTimer <= 0.0f; + && (statusUpdateTimer > NetConfig.HullUpdateInterval); - if (shouldSendStatusUpdate) + //force an update every 5 seconds even if nothing's changed (in case a client's gotten out of sync somehow) + if (shouldSendStatusUpdate || statusUpdateTimer > NetConfig.SparseHullUpdateInterval) { - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); - + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); lastSentVolume = waterVolume; lastSentOxygen = OxygenPercentage; lastSentFireCount = FireSources.Count; - - statusUpdateTimer = NetConfig.SparseHullUpdateInterval; + statusUpdateTimer = 0; } - if (decalUpdatePending && decalUpdateTimer <= 0.0f) + if (decalUpdatePending && decalUpdateTimer > NetConfig.HullUpdateInterval) { GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); - decalUpdateTimer = NetConfig.HullUpdateInterval; + decalUpdateTimer = 0; decalUpdatePending = false; } - if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer <= 0.0f) + if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer > NetConfig.HullUpdateInterval) { foreach (int pendingSectionUpdate in pendingSectionUpdates) { GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } - backgroundSectionUpdateTimer = NetConfig.HullUpdateInterval; + backgroundSectionUpdateTimer = 0; pendingSectionUpdates.Clear(); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 9d64d3876..075a0bd30 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -10,7 +10,8 @@ namespace Barotrauma.Networking c.KickAFKTimer = 0.0f; UInt16 ID = msg.ReadUInt16(); - ChatMessageType type = (ChatMessageType)msg.ReadByte(); + ChatMessageType type = (ChatMessageType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + ChatMode chatMode = (ChatMode)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMode)).Length - 1); string txt; Character orderTargetCharacter = null; @@ -172,7 +173,7 @@ namespace Barotrauma.Networking } else { - GameMain.Server.SendChatMessage(txt, null, c); + GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode); } } @@ -203,7 +204,7 @@ namespace Barotrauma.Networking { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)Type); + msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.Write((byte)ChangeType); msg.Write(Text); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 5a31dc4cf..fe720421c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -21,10 +21,10 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID = 0; public UInt16 LastRecvEntityEventID = 0; - public UInt16 LastRecvCampaignUpdate = 0; + public readonly Dictionary LastRecvCampaignUpdate = new Dictionary(); public UInt16 LastRecvCampaignSave = 0; - public Pair LastCampaignSaveSendTime; + public (UInt16 saveId, float time) LastCampaignSaveSendTime; public readonly List ChatMsgQueue = new List(); public UInt16 LastChatMsgQueueID; @@ -73,6 +73,9 @@ namespace Barotrauma.Networking characterInfo = value; } } + + public string PendingName; + public NetworkConnection Connection { get; set; } public bool SpectateOnly; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 7d5385f7f..b79b94e64 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -391,7 +391,7 @@ namespace Barotrauma.Networking StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { - client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); + client.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index ade75cd83..54bbd573c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading.Tasks; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 96651f8ec..f9085de45 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -746,7 +746,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); - CampaignSettings settings = new CampaignSettings(inc); + CampaignSettings settings = INetSerializableStruct.Read(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); @@ -767,8 +767,7 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { - ServerSettings.RadiationEnabled = settings.RadiationEnabled; - ServerSettings.MaxMissionCount = settings.MaxMissionCount; + ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } @@ -833,6 +832,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.EVENTMANAGER_RESPONSE: GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient); break; + case ClientPacketHeader.UPDATE_CHARACTERINFO: + UpdateCharacterInfo(inc, connectedClient); + break; case ClientPacketHeader.ERROR: HandleClientError(inc, connectedClient); break; @@ -955,7 +957,9 @@ namespace Barotrauma.Networking } if (Level.Loaded != null) { - errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); + errorLines.Add("Level: " + Level.Loaded.Seed + ", " + + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv + => cv.Key + "=" + cv.Value.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) @@ -1042,15 +1046,17 @@ namespace Barotrauma.Networking c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID); c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID); - TryChangeClientName(c, inc); + ReadClientNameChange(c, inc); c.LastRecvCampaignSave = inc.ReadUInt16(); if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1059,7 +1065,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1120,9 +1130,11 @@ namespace Barotrauma.Networking if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1131,7 +1143,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1368,7 +1384,7 @@ namespace Barotrauma.Networking if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedOutpost && save) + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); @@ -1548,11 +1564,11 @@ namespace Barotrauma.Networking NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) { //already sent an up-to-date campaign save - if (c.LastCampaignSaveSendTime != null && campaign.LastSaveID == c.LastCampaignSaveSendTime.First) + if (c.LastCampaignSaveSendTime != default && campaign.LastSaveID == c.LastCampaignSaveSendTime.saveId) { //the save was sent less than 5 second ago, don't attempt to resend yet //(the client may have received it but hasn't acked us yet) - if (c.LastCampaignSaveSendTime.Second > NetTime.Now - 5.0f) + if (c.LastCampaignSaveSendTime.time > NetTime.Now - 5.0f) { return; } @@ -1561,7 +1577,7 @@ namespace Barotrauma.Networking if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); - c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); + c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now); } } } @@ -1670,8 +1686,7 @@ namespace Barotrauma.Networking outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server outmsg.Write(c.LastSentEntityEventID); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -1897,8 +1912,7 @@ namespace Barotrauma.Networking int campaignBytes = outmsg.LengthBytes; var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -2047,7 +2061,10 @@ namespace Barotrauma.Networking var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); msg.Write(campaign == null ? (UInt16)0 : campaign.LastSaveID); - msg.Write(campaign == null ? (UInt16)0 : campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + msg.Write(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag)); + } connectedClients.ForEach(c => c.ReadyToStart = false); @@ -2075,7 +2092,7 @@ namespace Barotrauma.Networking } } - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Unsure), false); + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Empty), false); yield return CoroutineStatus.Success; } @@ -2193,7 +2210,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); - AutoItemPlacer.PlaceIfNeeded(); + AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet); CrewManager crewManager = campaign?.CrewManager; @@ -2388,7 +2405,9 @@ namespace Barotrauma.Networking } campaign?.LoadPets(); - crewManager?.LoadActiveOrders(); + campaign?.LoadActiveOrders(); + + campaign?.CargoManager.InitPurchasedIDCards(); foreach (Submarine sub in Submarine.MainSubs) { @@ -2400,7 +2419,7 @@ namespace Barotrauma.Networking spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } - CargoManager.CreateItems(spawnList, sub); + CargoManager.CreateItems(spawnList, sub, cargoManager: null); } TraitorManager = null; @@ -2461,6 +2480,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); + msg.Write(serverSettings.MaximumMoneyTransferRequest); msg.Write(IsUsingRespawnShuttle()); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); @@ -2531,10 +2551,9 @@ namespace Barotrauma.Networking { msg.Write(mission.Prefab.Identifier); } - msg.Write((byte)GameMain.GameSession.Level.EqualityCheckValues.Count); - foreach (int equalityCheckValue in GameMain.GameSession.Level.EqualityCheckValues) + foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType().OrderBy(s => s)) { - msg.Write(equalityCheckValue); + msg.Write(GameMain.GameSession.Level.EqualityCheckValues[stage]); } foreach (Mission mission in GameMain.GameSession.Missions) { @@ -2656,7 +2675,7 @@ namespace Barotrauma.Networking base.AddChatMessage(message); } - private bool TryChangeClientName(Client c, IReadMessage inc) + private bool ReadClientNameChange(Client c, IReadMessage inc) { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); @@ -2666,16 +2685,40 @@ namespace Barotrauma.Networking if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } c.NameID = nameId; - newName = Client.SanitizeName(newName); if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } c.PreferredJob = newJob; c.PreferredTeam = newTeam; + return TryChangeClientName(c, newName); + } + + public bool TryChangeClientName(Client c, string newName) + { + newName = Client.SanitizeName(newName); //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is LastClientListUpdateID++; - if (newName == c.Name) { return false; } + if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; } + + if (IsNameValid(c, newName)) + { + string oldName = c.Name; + c.Name = newName; + c.Connection.Name = newName; + SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); + return true; + } + else + { + return false; + } + } + + + private bool IsNameValid(Client c, string newName) + { + newName = Client.SanitizeName(newName); if (c.Connection != OwnerConnection) { @@ -2704,9 +2747,6 @@ namespace Barotrauma.Networking return false; } - SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={c.Name}~[newname]={newName}", ChatMessageType.Server); - c.Name = newName; - c.Connection.Name = newName; return true; } @@ -2948,7 +2988,7 @@ namespace Barotrauma.Networking /// /// Add the message to the chatbox and pass it to all clients who can receive it /// - public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, ChatMode chatMode = ChatMode.None) { string senderName = ""; @@ -3004,6 +3044,10 @@ namespace Barotrauma.Networking type = ChatMessageType.Private; } + else if (chatMode == ChatMode.Radio) + { + type = ChatMessageType.Radio; + } else { type = ChatMessageType.Default; @@ -3032,7 +3076,6 @@ namespace Barotrauma.Networking { senderCharacter = senderClient.Character; senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; - if (type == ChatMessageType.Private) { if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) @@ -3178,9 +3221,9 @@ namespace Barotrauma.Networking Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection); if (transfer.FileType == FileTransferType.CampaignSave && (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) && - recipient.LastCampaignSaveSendTime != null) + recipient.LastCampaignSaveSendTime != default) { - recipient.LastCampaignSaveSendTime.Second = (float)Lidgren.Network.NetTime.Now; + recipient.LastCampaignSaveSendTime.time = (float)NetTime.Now; } } @@ -3199,18 +3242,27 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 1); - int max = GameMain.Server.ConnectedClients.Count(c => c.InGame); - // Required ratio cannot be met - if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) - { - Voting.ActiveVote.Finish(Voting, passed: false); - } - else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (inGameClients.Count() == 1) { Voting.ActiveVote.Finish(Voting, passed: true); - } + } + else + { + var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); + int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 1); + int max = eligibleClients.Count(); + // Required ratio cannot be met + if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: false); + } + else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: true); + } + } } Client.UpdateKickVotes(connectedClients); @@ -3291,7 +3343,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); } Voting.StopSubmarineVote(true); @@ -3484,6 +3536,24 @@ namespace Barotrauma.Networking return; } + string newName = message.ReadString(); + if (string.IsNullOrEmpty(newName)) + { + newName = sender.Name; + } + else + { + newName = Client.SanitizeName(newName); + if (!IsNameValid(sender, newName)) + { + newName = sender.Name; + } + else + { + sender.PendingName = newName; + } + } + int tagCount = message.ReadByte(); HashSet tagSet = new HashSet(); for (int i = 0; i < tagCount; i++) @@ -3511,7 +3581,7 @@ namespace Barotrauma.Networking } } - sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); + 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; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 8fe44eb71..e86a858f5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -278,12 +278,12 @@ namespace Barotrauma static bool isValid(Item item) { - return item.Prefab.Identifier == "idcard" || item.GetComponent() != null || item.GetComponent() != null; + return item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null; } if (foundItem == null) { return; } - bool isIdCard = ((MapEntity)foundItem).Prefab.Identifier == "idcard"; + bool isIdCard = foundItem.GetComponent() != null; bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; if (isIdCard) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index f2cef0e0e..d8d509a43 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -6,7 +8,7 @@ { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)ChatMessageType.Order); + msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.Write(SenderName); msg.Write(SenderClient != null); if (SenderClient != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index b11700a19..de685bebf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -390,8 +390,16 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); bool forceSpawnInMainSub = false; - if (!bot && campaign != null) + if (!bot) { + //the client has opted to change the name of their new character + //when the character spawns, set the client's name to match + if (clients[i].PendingName == characterInfos[i].Name) + { + GameMain.Server?.TryChangeClientName(clients[i], clients[i].PendingName); + clients[i].PendingName = null; + } + var matchingData = campaign?.GetClientCharacterData(clients[i]); if (matchingData != null) { @@ -441,32 +449,43 @@ namespace Barotrauma.Networking GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", GameServer.ClientLogName(clients[i]), clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); } - if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null) + if (RespawnShuttle != null) { Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position; - if (divingSuitPrefab != null && oxyPrefab != null) + if (divingSuitPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); respawnItems.Add(divingSuit); - var oxyTank = new Item(oxyPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); - divingSuit.Combine(oxyTank, user: null); - respawnItems.Add(oxyTank); + if (oxyPrefab != null && divingSuit.GetComponent() != null) + { + var oxyTank = new Item(oxyPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); + divingSuit.Combine(oxyTank, user: null); + respawnItems.Add(oxyTank); + } } - if (scooterPrefab != null && batteryPrefab != null) + if (!(GameMain.GameSession.GameMode is CampaignMode)) { - var scooter = new Item(scooterPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); - - var battery = new Item(batteryPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); - - scooter.Combine(battery, user: null); - respawnItems.Add(scooter); - respawnItems.Add(battery); + if (scooterPrefab != null) + { + var scooter = new Item(scooterPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); + respawnItems.Add(scooter); + if (batteryPrefab != null) + { + var battery = new Item(batteryPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); + scooter.Combine(battery, user: null); + respawnItems.Add(battery); + } + } + } + if (respawnContainer != null) + { + AutoItemPlacer.RegenerateLoot(RespawnShuttle, respawnContainer); } } @@ -504,7 +523,7 @@ namespace Barotrauma.Networking //add the ID card tags they should've gotten when spawning in the shuttle foreach (Item item in character.Inventory.AllItems.Distinct()) { - if (item.Prefab.Identifier != "idcard") { continue; } + if (item.GetComponent() == null) { continue; } foreach (string s in shuttleSpawnPoints[i].IdCardTags) { item.AddTag(s); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index bc841e5a9..39f7833c9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -1,11 +1,9 @@ -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.IO; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -36,7 +34,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); private bool IsFlagRequired(Client c, NetFlags flag) - => LastUpdateIdForFlag[flag] > c.LastRecvLobbyUpdate; + => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys @@ -56,7 +54,7 @@ namespace Barotrauma.Networking { var property = netProperties[key]; property.SyncValue(); - if (property.LastUpdateID > c.LastRecvLobbyUpdate) + if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) { outMsg.Write(key); netProperties[key].Write(outMsg); @@ -257,7 +255,7 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("queryport", QueryPort); #endif doc.Root.SetAttributeValue("password", password ?? ""); - + doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); @@ -266,11 +264,12 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); - + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); + doc.Root.Add(CampaignSettings.Save()); System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { @@ -399,7 +398,7 @@ namespace Barotrauma.Networking ServerName = doc.Root.GetAttributeString("name", ""); if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); - + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; //handle Random as the mission type, which is no longer a valid setting //MissionType.All offers equivalent functionality @@ -410,6 +409,14 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + + foreach (XElement element in doc.Root.Elements()) + { + if (element.Name.ToIdentifier() == nameof(Barotrauma.CampaignSettings)) + { + CampaignSettings = new CampaignSettings(element); + } + } } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index f8a129f51..4a3577fe5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -27,11 +27,13 @@ namespace Barotrauma public VoteState State { get; set; } public SubmarineInfo Sub; + public bool TransferItems; public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) { Sub = subInfo; + TransferItems = transferItems; DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; @@ -44,12 +46,14 @@ namespace Barotrauma { GameMain.Server?.SwitchSubmarine(); } + else + { + voting.RegisterRejectedVote(this); + } voting.StopSubmarineVote(passed); } } - public static IVote ActiveVote; - public class TransferVote : IVote { public Client VoteStarter { get; } @@ -83,21 +87,28 @@ namespace Barotrauma toWallet.Give(TransferAmount); } } + else + { + voting.RegisterRejectedVote(this); + } voting.StopMoneyTransferVote(passed); } } + public static IVote ActiveVote; + private static readonly Queue pendingVotes = new Queue(); - private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) + private readonly TimeSpan rejectedVoteCooldown = new TimeSpan(0, 1, 0); + + private readonly Dictionary rejectedVoteTimes = new Dictionary(); + + private void StartSubmarineVote(SubmarineInfo subInfo, bool transferItems, VoteType voteType, Client sender) { - if (ActiveVote == null) - { - sender.SetVote(voteType, 2); - } var subVote = new SubmarineVote( sender, subInfo, + transferItems, voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); @@ -136,9 +147,9 @@ namespace Barotrauma public void StartTransferVote(Client starter, Client from, int transferAmount, Client to) { - if (ActiveVote == null) + if (ShouldRejectVote(starter, VoteType.TransferMoney)) { - starter.SetVote(VoteType.TransferMoney, 2); + return; } StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to)); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); @@ -156,6 +167,31 @@ namespace Barotrauma } } + private bool ShouldRejectVote(Client sender, VoteType voteType) + { + if (rejectedVoteTimes.ContainsKey(sender)) + { + TimeSpan remainingCooldown = (rejectedVoteTimes[sender].time + rejectedVoteCooldown) - DateTime.Now; + if (rejectedVoteTimes[sender].voteType == voteType && + remainingCooldown.TotalSeconds > 0) + { + GameMain.Server.SendDirectChatMessage( + TextManager.FormatServerMessage("voterejectedpleasewait", ("[time]", ((int)remainingCooldown.TotalSeconds).ToString())), + sender, ChatMessageType.ServerMessageBox); + return true; + } + } + return false; + } + + protected void RegisterRejectedVote(IVote vote) + { + if (vote.VoteStarter != null) + { + rejectedVoteTimes[vote.VoteStarter] = (vote.VoteType, DateTime.Now); + } + } + public void Update(float deltaTime) { if (ActiveVote == null) { return; } @@ -164,10 +200,19 @@ namespace Barotrauma if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) { + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); + // Do not take unanswered into account for total - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); - ActiveVote.Finish(this, passed: yes / (float)(yes + no) >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio); + int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); + int total = Math.Max(yes + no, 1); + + bool passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + + ActiveVote.Finish(this, passed); } } @@ -227,7 +272,6 @@ namespace Barotrauma GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); } break; - case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: @@ -240,18 +284,26 @@ namespace Barotrauma int amount = inc.ReadInt32(); int fromClientId = inc.ReadByte(); int toClientId = inc.ReadByte(); - pendingVotes.Enqueue(new TransferVote(sender, - GameMain.Server.ConnectedClients.Find(c => c.ID == fromClientId), - amount, - GameMain.Server.ConnectedClients.Find(c => c.ID == toClientId))); + if (!ShouldRejectVote(sender, voteType)) + { + pendingVotes.Enqueue(new TransferVote(sender, + GameMain.Server.ConnectedClients.Find(c => c.ID == fromClientId), + amount, + GameMain.Server.ConnectedClients.Find(c => c.ID == toClientId))); + } } else { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + bool transferItems = inc.ReadBoolean(); + if (!ShouldRejectVote(sender, voteType)) { - StartSubmarineVote(subInfo, voteType, sender); + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && + (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + { + StartSubmarineVote(subInfo, transferItems, voteType, sender); + } } } } @@ -307,22 +359,24 @@ namespace Barotrauma { msg.Write((byte)ActiveVote.VoteType); if (ActiveVote.State != VoteState.None && ActiveVote.VoteType != VoteType.Unknown) - { - var yesClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - msg.Write((byte)yesClients.Count); + { + var eligibleClients = GameMain.Server.ConnectedClients.Where(c => c.InGame && c != ActiveVote.VoteStarter); + + var yesClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 2); + msg.Write((byte)yesClients.Count()); foreach (Client c in yesClients) { msg.Write(c.ID); } - var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); - msg.Write((byte)noClients.Count); + var noClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 1); + msg.Write((byte)noClients.Count()); foreach (Client c in noClients) { msg.Write(c.ID); } - msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.InGame)); + msg.Write((byte)eligibleClients.Count()); switch (ActiveVote.State) { @@ -336,6 +390,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: msg.Write((ActiveVote as SubmarineVote).Sub.Name); + msg.Write((ActiveVote as SubmarineVote).TransferItems); break; case VoteType.TransferMoney: var transferVote = (ActiveVote as TransferVote); @@ -357,8 +412,10 @@ namespace Barotrauma case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: - msg.Write((ActiveVote as SubmarineVote).Sub.Name); - msg.Write((short)(ActiveVote as SubmarineVote).DeliveryFee); + var subVote = ActiveVote as SubmarineVote; + msg.Write(subVote.Sub.Name); + msg.Write(subVote.TransferItems); + msg.Write((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index c54e862a1..6210c5922 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.16.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -60,10 +60,11 @@ - + + diff --git a/Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save b/Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save new file mode 100644 index 000000000..f6a00dd10 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save differ diff --git a/Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save b/Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save new file mode 100644 index 000000000..f892d0374 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save differ diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml new file mode 100644 index 000000000..d88b556aa --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 9c942ee08..cc9a9100e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -31,6 +31,17 @@ namespace Barotrauma if (_previousAiTarget != null) { _lastAiTarget = _previousAiTarget; + if (_selectedAiTarget != null) + { + if (_selectedAiTarget.Entity is Item i && _previousAiTarget.Entity is Character c) + { + if (i.IsOwnedBy(c)) { return; } + } + else if (_previousAiTarget.Entity is Item it && _selectedAiTarget.Entity is Character ch) + { + if (it.IsOwnedBy(ch)) { return; } + } + } } OnTargetChanged(_previousAiTarget, _selectedAiTarget); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index f13ea197e..82b3e7591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -34,14 +34,18 @@ namespace Barotrauma public float SoundRange { get { return soundRange; } - set + set { if (float.IsNaN(value)) { DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + if (soundRange > 0.0f && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -55,7 +59,11 @@ namespace Barotrauma DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + if (sightRange > 0 && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -99,13 +107,33 @@ namespace Barotrauma /// public bool InDetectable { - get => inDetectable || (SoundRange <= 0 && SightRange <= 0); - set => inDetectable = value; + get + { + return inDetectable || (SoundRange <= 0 && SightRange <= 0); + } + set + { + inDetectable = value; + if (inDetectable) + { + NeedsUpdate = true; + } + } } + public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; + /// + /// Does the AI target do something that requires Update() to be called (e.g. static targets don't need to be updated) + /// + public bool NeedsUpdate + { + get; + private set; + } = true; + public TargetType Type { get; private set; } public enum TargetType @@ -190,14 +218,22 @@ namespace Barotrauma if (!Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active - if (!StaticSight && SightRange > 0) + if (!StaticSight && sightRange > 0) { DecreaseSightRange(deltaTime); } - if (!StaticSound && SoundRange > 0) + if (!StaticSound && soundRange > 0) { DecreaseSoundRange(deltaTime); } + if (sightRange <= 0 && soundRange <= 0) + { + NeedsUpdate = false; + } + } + else + { + NeedsUpdate = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 0edb7fa1d..8897ebecb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -275,7 +275,11 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "chooserandom": - LoadSubElement(subElement.Elements().ToArray().GetRandom(random)); + var subElements = subElement.Elements(); + if (subElements.Any()) + { + LoadSubElement(subElements.ToArray().GetRandom(random)); + } break; default: LoadSubElement(subElement); @@ -1055,6 +1059,9 @@ namespace Barotrauma private Vector2 attackWorldPos; private Vector2 attackSimPos; + private float reachTimer; + // How long the monster tries to reach out for the target when it's close to it before ignoring it. + private const float reachTimeOut = 10; private void UpdateAttack(float deltaTime) { @@ -1427,6 +1434,41 @@ namespace Barotrauma // Check that we can reach the target distance = toTarget.Length(); canAttack = distance < AttackLimb.attack.Range; + if (canAttack) + { + reachTimer = 0; + } + else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) + { + Vector2 targetVelocity = Vector2.Zero; + Submarine targetSub = SelectedAiTarget.Entity.Submarine; + if (targetSub != null) + { + targetVelocity = targetSub.Velocity; + } + else if (targetCharacter != null) + { + targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity; + } + else if (SelectedAiTarget.Entity is Item i && i.body != null) + { + targetVelocity = i.body.LinearVelocity; + } + float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared(); + float targetSpeed = targetVelocity.LengthSquared(); + if (mySpeed < 0.1f || mySpeed > targetSpeed) + { + reachTimer += deltaTime; + if (reachTimer > reachTimeOut) + { + reachTimer = 0; + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + ResetAITarget(); + return; + } + } + } // Crouch if the target is down (only humanoids), so that we can reach it. if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) @@ -1958,9 +2000,8 @@ namespace Barotrauma } if (!isFriendly && attackResult.Damage > 0.0f) { - ignoredTargets.Remove(attacker.AiTarget); bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; - if (AIParams.AttackWhenProvoked && canAttack) + if (AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.AiTarget)) { if (attacker.IsHusk) { @@ -3476,6 +3517,7 @@ namespace Barotrauma { observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); } + reachTimer = 0; } protected override void OnStateChanged(AIState from, AIState to) @@ -3496,6 +3538,7 @@ namespace Barotrauma SetStateResetTimer(); } blockCheckTimer = 0; + reachTimer = 0; } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 651c3bbf9..ff7c37218 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -59,7 +59,11 @@ namespace Barotrauma private readonly float enemyCheckInterval = 0.2f; private readonly float enemySpotDistanceOutside = 800; private readonly float enemySpotDistanceInside = 1000; - private float enemycheckTimer; + private float enemyCheckTimer; + + private readonly float reportProblemsInterval = 1.0f; + private float reportProblemsTimer; + /// /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. @@ -166,6 +170,7 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); SortTimer = Rand.Range(0f, sortObjectiveInterval); + reportProblemsTimer = Rand.Range(0f, reportProblemsInterval); } public override void Update(float deltaTime) @@ -309,10 +314,10 @@ namespace Barotrauma { // 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 - enemycheckTimer -= deltaTime; - if (enemycheckTimer < 0) + enemyCheckTimer -= deltaTime; + if (enemyCheckTimer < 0) { - enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); + enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); if (!objectiveManager.IsCurrentObjective()) { float closestDistance = 0; @@ -407,19 +412,29 @@ namespace Barotrauma { if (Character.IsOnPlayerTeam) { - VisibleHulls.ForEach(h => PropagateHullSafety(Character, h)); + foreach (Hull h in VisibleHulls) + { + PropagateHullSafety(Character, h); + } } else { - // Outpost npcs don't inform each other about threats, like crew members do. - VisibleHulls.ForEach(h => RefreshHullSafety(h)); + foreach (Hull h in VisibleHulls) + { + RefreshHullSafety(h); + } } } if (Character.SpeechImpediment < 100.0f) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + reportProblemsTimer -= deltaTime; + if (reportProblemsTimer <= 0.0f) { - ReportProblems(); + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + { + ReportProblems(); + } + reportProblemsTimer = reportProblemsInterval; } UpdateSpeaking(); } @@ -785,9 +800,10 @@ namespace Barotrauma if (item == null || item.Removed) { return; } if (!itemsToRelocate.Contains(item)) { return; } var mainSub = Submarine.MainSub; - if (item.ParentInventory != null) + Entity owner = item.GetRootInventoryOwner(); + if (owner != null) { - if (item.ParentInventory.Owner is Character c) + if (owner is Character c) { if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) { @@ -795,24 +811,37 @@ namespace Barotrauma return; } } - else if (item.ParentInventory.Owner.Submarine == mainSub) + else if (owner.Submarine == mainSub) { // Placed inside an inventory that's already in the main sub. return; } } - // Laying on ground inside the main sub. + // Laying on the ground inside the main sub. if (item.Submarine == mainSub) { return; } - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub); - if (wp != null) + if (owner != null && owner != item) { - item.Submarine = mainSub; - item.SetTransform(wp.SimPosition, 0.0f); + item.Drop(null); + } + item.Submarine = mainSub; + Item newContainer = mainSub.FindContainerFor(item, onlyPrimary: false); + if (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null)) + { + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub) ?? WayPoint.GetRandom(SpawnType.Path, null, mainSub); + if (wp != null) + { + item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false); + } + else + { + DebugConsole.AddWarning($"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + } } itemsToRelocate.Remove(item); + DebugConsole.Log($"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub."); } } @@ -837,8 +866,8 @@ namespace Barotrauma var container = i.GetComponent(); if (container == null) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } - var rootContainer = container.Item.GetRootContainer(); - if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } + var rootContainer = container.Item.GetRootContainer() ?? container.Item; + if (rootContainer.GetComponent() != null || rootContainer.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) @@ -853,7 +882,12 @@ namespace Barotrauma } else { - return isPreferencesDefined ? 0 : 1; + if (isPreferencesDefined) + { + // Use any valid locker as a fall back container. + return container.Item.HasTag("locker") ? 0.5f : 0; + } + return 1; } } } @@ -1149,7 +1183,7 @@ namespace Barotrauma bool isAttackerFightingEnemy = false; float minorDamageThreshold = 1; float majorDamageThreshold = 20; - if (attacker.TeamID == Character.TeamID) + if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator) { minorDamageThreshold = 10; majorDamageThreshold = 40; @@ -1356,6 +1390,10 @@ namespace Barotrauma Character FindInstigator() { + if (Character.IsInstigator) + { + return Character; + } if (attacker.IsInstigator) { return attacker; @@ -1545,7 +1583,7 @@ namespace Barotrauma (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -1889,7 +1927,7 @@ namespace Barotrauma float fireFactor = 1; if (!ignoreFire) { - float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; + static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; // Even the smallest fire reduces the safety by 50% float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h)); fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); @@ -1897,20 +1935,32 @@ namespace Barotrauma float enemyFactor = 1; if (!ignoreEnemies) { - bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested; - int enemyCount = visibleHulls == null ? - Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) : - Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull)); + int enemyCount = 0; + foreach (Character c in Character.CharacterList) + { + if (visibleHulls == null) + { + if (c.CurrentHull != hull) { continue; } + } + else + { + if (!visibleHulls.Contains(c.CurrentHull)) { continue; } + } + if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested) + { + enemyCount++; + } + } // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } float dangerousItemsFactor = 1f; - foreach (Item item in Item.ItemList) + foreach (Item item in Item.DangerousItems) { - if (item.CurrentHull != hull) { continue; } - if (item.Prefab != null && item.Prefab.IsDangerous) - { + if (item.CurrentHull == hull) + { dangerousItemsFactor = 0; + break; } } float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 5e618f9e7..1a14bd206 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -245,7 +245,7 @@ namespace Barotrauma { get { - if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index c647b82d0..1b2f390e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -2,7 +2,6 @@ using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; -using System; namespace Barotrauma { @@ -48,6 +47,9 @@ namespace Barotrauma protected override bool Filter(Item target) { + System.Diagnostics.Debug.Assert(target.GetComponent() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items."); + System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined."); + // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } @@ -57,7 +59,7 @@ namespace Barotrauma return true; } - protected override IEnumerable GetList() => Item.ItemList; + protected override IEnumerable GetList() => Item.CleanableItems; protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier) @@ -102,9 +104,6 @@ namespace Barotrauma } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } - var pickable = item.GetComponent(); - if (pickable == null) { return false; } - if (pickable is Holdable h && h.Attachable && h.Attached) { return false; } var wire = item.GetComponent(); if (wire != null) { @@ -113,15 +112,11 @@ namespace Barotrauma else { var connectionPanel = item.GetComponent(); - if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null))) + if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Count > 0)) { return false; } } - if (item.Prefab.PreferredContainers.None()) - { - return false; - } if (!checkInventory) { return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index bdc673490..c3a21854d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using FarseerPhysics.Dynamics; +using static Barotrauma.AIObjectiveFindSafety; namespace Barotrauma { @@ -775,7 +776,13 @@ namespace Barotrauma } else { - retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + if (hullSearchStatus != HullSearchStatus.Finished) + { + findSafety.UpdateSimpleEscape(deltaTime); + return; + } + retreatTarget = potentialSafeHull; findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); } } @@ -785,21 +792,21 @@ namespace Barotrauma { UsePathingOutside = false }, - onAbandon: () => + onAbandon: () => + { + if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) { - if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) - { - // If in the same room with an enemy -> don't try to escape because we'd want to fight it - SteeringManager.Reset(); - RemoveSubObjective(ref retreatObjective); - } - else - { - // else abandon and fall back to find safety mode - Abandon = true; - } - }, - onCompleted: () => RemoveSubObjective(ref retreatObjective)); + // If in the same room with an enemy -> don't try to escape because we'd want to fight it + SteeringManager.Reset(); + RemoveSubObjective(ref retreatObjective); + } + else + { + // else abandon and fall back to find safety mode + Abandon = true; + } + }, + onCompleted: () => RemoveSubObjective(ref retreatObjective)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index f3380c63a..2eb5b453e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -192,9 +193,17 @@ namespace Barotrauma } else { + HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + if (hullSearchStatus != HullSearchStatus.Finished) + { + UpdateSimpleEscape(deltaTime); + return; + } + searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; - currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + currentSafeHull = potentialSafeHull; + cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); if (currentSafeHull == null) { @@ -250,58 +259,122 @@ namespace Barotrauma } } if (subObjectives.Any(so => so.CanBeCompleted)) { return; } - if (currentHull != null) + UpdateSimpleEscape(deltaTime); + } + } + + public void UpdateSimpleEscape(float deltaTime) + { + Vector2 escapeVel = Vector2.Zero; + if (character.CurrentHull != null) + { + foreach (Hull hull in HumanAIController.VisibleHulls) { - //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) - // -> attempt to manually steer away from hazards - Vector2 escapeVel = Vector2.Zero; - foreach (Hull hull in HumanAIController.VisibleHulls) + foreach (FireSource fireSource in hull.FireSources) { - foreach (FireSource fireSource in hull.FireSources) - { - Vector2 dir = character.Position - fireSource.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); - } - } - foreach (Character enemy in Character.CharacterList) - { - if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } - if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) - { - Vector2 dir = character.Position - enemy.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); - } - } - if (escapeVel != Vector2.Zero) - { - float left = currentHull.Rect.X + 50; - float right = currentHull.Rect.Right - 50; - //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.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); - } - else - { - character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; - character.AIController.SteeringManager.Reset(); - } - return; + Vector2 dir = character.Position - fireSource.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); } } + foreach (Character enemy in Character.CharacterList) + { + if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } + if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) + { + Vector2 dir = character.Position - enemy.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); + } + } + } + if (escapeVel != Vector2.Zero) + { + float left = character.CurrentHull.Rect.X + 50; + float right = character.CurrentHull.Rect.Right - 50; + //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.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); + } + else + { + character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; + character.AIController.SteeringManager.Reset(); + } + } + else + { objectiveManager.GetObjective().Wander(deltaTime); } } - public Hull FindBestHull(IEnumerable ignoredHulls = null, bool allowChangingTheSubmarine = true) + public enum HullSearchStatus { - //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 - //path calculations, only to discard all of them when going through the hulls in the outpost) - float EstimateHullSuitability(Hull hull) + Running, + Finished + } + + private readonly List hulls = new List(); + private int hullSearchIndex = -1; + float bestHullValue = 0; + bool bestHullIsAirlock = false; + Hull potentialBestHull; + + /// + /// Tries to find the best (safe, nearby) hull the character can find a path to. + /// Checks one hull at a time, and returns HullSearchStatus.Finished when all potential hulls have been checked. + /// + public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable ignoredHulls = null, bool allowChangingSubmarine = true) + { + if (hullSearchIndex == -1) + { + bestHullValue = 0; + potentialBestHull = null; + bestHullIsAirlock = false; + hulls.Clear(); + var connectedSubs = character.Submarine?.GetConnectedSubs(); + foreach (Hull hull in Hull.HullList) + { + if (hull.Submarine == null) { continue; } + // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. + if (hull.Submarine.Info.IsRuin) { continue; } + if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; } + if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } + 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 + //path calculations, only to discard all of them when going through the hulls in the outpost) + float hullSuitability = EstimateHullSuitability(character, hull); + if (!hulls.Any()) + { + hulls.Add(hull); + } + else + { + for (int i = 0; i < hulls.Count; i++) + { + if (hullSuitability > EstimateHullSuitability(character, hulls[i])) + { + hulls.Insert(i, hull); + break; + } + } + } + } + if (hulls.None()) + { + bestHull = null; + return HullSearchStatus.Finished; + } + hullSearchIndex = 0; + } + + static float EstimateHullSuitability(Character character, Hull hull) { float dist = Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) + @@ -314,86 +387,91 @@ namespace Barotrauma return suitability; } - Hull bestHull = null; - float bestValue = 0; - bool bestIsAirlock = false; - foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h))) + Hull potentialHull = hulls[hullSearchIndex]; + + float hullSafety = 0; + bool hullIsAirlock = false; + bool isCharacterInside = character.CurrentHull != null && character.Submarine != null; + if (isCharacterInside) { - if (hull.Submarine == null) { continue; } - // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. - if (hull.Submarine.Info.IsRuin) { continue; } - if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; } - if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } - if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } - if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } - float hullSafety = 0; - bool hullIsAirlock = false; - bool isCharacterInside = character.CurrentHull != null && character.Submarine != null; - if (isCharacterInside) - { - if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } - hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character); - float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); - hullSafety *= distanceFactor; - //skip the hull if the safety is already less than the best hull - //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) - if (hullSafety < bestValue) { continue; } + hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character); + float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); + hullSafety *= distanceFactor; + //skip the hull if the safety is already less than the best hull + //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) + if (hullSafety > bestHullValue) + { //avoid airlock modules if not allowed to change the sub - if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock")) + if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock")) { - continue; + // Don't allow to go outside if not already outside. + var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) + { + hullSafety = 0; + HumanAIController.UnreachableHulls.Add(potentialHull); + } + else + { + // Each unsafe node reduces the hull safety value. + // Ignore the current hull, because otherwise we couldn't find a path out. + int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); + hullSafety /= 1 + unsafeNodes; + // If the target is not inside a friendly submarine, considerably reduce the hull safety. + if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true)) + { + hullSafety /= 10; + } + } } - // Don't allow to go outside if not already outside. - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) + else { - HumanAIController.UnreachableHulls.Add(hull); - continue; + hullSafety = 0; } - // Each unsafe node reduces the hull safety value. - // Ignore the current hull, because otherwise we couldn't find a path out. - int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); - hullSafety /= 1 + unsafeNodes; - // If the target is not inside a friendly submarine, considerably reduce the hull safety. - if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) - { - hullSafety /= 10; - } - } - else - { - // TODO: could also target gaps that get us inside? - if (hull.IsTaggedAirlock()) - { - hullSafety = 100; - hullIsAirlock = true; - } - else if(!bestIsAirlock && hull.LeadsOutside(character)) - { - hullSafety = 100; - } - // Huge preference for closer targets - float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); - hullSafety *= distanceFactor; - // If the target is not inside a friendly submarine, considerably reduce the hull safety. - // Intentionally exclude wrecks from this check - if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC) - { - hullSafety /= 10; - } - } - if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock)) - { - bestHull = hull; - bestValue = hullSafety; - bestIsAirlock = hullIsAirlock; } } - return bestHull; + else + { + // TODO: could also target gaps that get us inside? + if (potentialHull.IsTaggedAirlock()) + { + hullSafety = 100; + hullIsAirlock = true; + } + else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character)) + { + hullSafety = 100; + } + // Huge preference for closer targets + float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); + hullSafety *= distanceFactor; + // If the target is not inside a friendly submarine, considerably reduce the hull safety. + // Intentionally exclude wrecks from this check + if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC) + { + hullSafety /= 10; + } + } + if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock)) + { + potentialBestHull = potentialHull; + bestHullValue = hullSafety; + bestHullIsAirlock = hullIsAirlock; + } + + bestHull = potentialBestHull; + hullSearchIndex++; + + if (hullSearchIndex >= hulls.Count) + { + hullSearchIndex = -1; + return HullSearchStatus.Finished; + } + return HullSearchStatus.Running; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 99adb5e1d..a2567e72b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -48,16 +48,29 @@ namespace Barotrauma } 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)); - float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float reduction = isPriority ? 1 : 2; - float max = AIObjectiveManager.LowestOrderPriority - reduction; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; + if (operateObjective != null && objectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + { + // 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)); + } } return Priority; } @@ -165,6 +178,7 @@ namespace Barotrauma requiredCondition = () => Leak.Submarine == character.Submarine && Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h), + endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() }, @@ -201,7 +215,7 @@ namespace Barotrauma // This is an approximation, because we don't know the exact reach until the pose is taken. // And even then the actual range depends on the direction we are aiming to. // Found out that without any multiplier the value (209) is often too short. - return repairTool.Range + armLength * 1.3f; + return repairTool.Range + armLength * 2; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 4b5aba5dc..853abb82e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -471,7 +471,8 @@ namespace Barotrauma { if (spawnItemIfNotFound) { - if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) + ItemPrefab prefab = FindItemToSpawn(); + if (prefab == null) { #if DEBUG DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); @@ -501,6 +502,33 @@ namespace Barotrauma } } + /// + /// Returns the "best" item to spawn when using and there's multiple suitable items. + /// Best in this context is the one that's sold at the lowest price in stores (usually the most "basic" item) + /// + /// + private ItemPrefab FindItemToSpawn() + { + ItemPrefab bestItem = null; + float lowestCost = float.MaxValue; + foreach (MapEntityPrefab prefab in MapEntityPrefab.List) + { + if (!(prefab is ItemPrefab itemPrefab)) { continue; } + if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id))) + { + float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ? + itemPrefab.DefaultPrice.Price : + float.MaxValue; + if (cost < lowestCost || bestItem == null) + { + bestItem = itemPrefab; + lowestCost = cost; + } + } + } + return bestItem; + } + protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 762e8cd08..5b5665bf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -185,6 +185,11 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } else { @@ -290,12 +295,25 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } } public void Wander(float deltaTime) { - if (character.IsClimbing) { return; } + if (character.IsClimbing) + { + if (character.AnimController.GetHeightFromFloor() < 0.1f) + { + character.AnimController.Anim = AnimController.Animation.None; + character.SelectedConstruction = null; + } + return; + } var currentHull = character.CurrentHull; if (!character.AnimController.InWater && currentHull != null) { @@ -470,7 +488,7 @@ namespace Barotrauma if (hull != null) { itemsToClean.Clear(); - foreach (Item item in Item.ItemList) + foreach (Item item in Item.CleanableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index b13723512..a5b9338dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -39,12 +39,21 @@ namespace Barotrauma { TargetContainers.Add(targetContainer); } + else + { + foreach (Item item in Item.ItemList) + { + if (!OrderPrefab.TargetItemsMatchItem(TargetContainerTags, item)) { continue; } + TargetContainers.Add(item); + } + } TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full; } protected override bool Filter(Item target) { - if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; } + //don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers) + if (!IsValidTarget(target, character, null, TargetCondition)) { return false; } if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } return true; @@ -52,8 +61,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) { - if (item == null) { return false; } - if (item.Removed) { return false; } + 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 (container.Inventory == null) { return false; } @@ -88,7 +96,7 @@ namespace Barotrauma } } - protected override IEnumerable GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList; + protected override IEnumerable GetList() => TargetContainers; protected override AIObjective ObjectiveConstructor(Item target) => new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 9b05c7e0a..652ce82a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -142,7 +142,7 @@ namespace Barotrauma } var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index c908ac461..eb232b4f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -13,7 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; - private IEnumerable pumpList; + private List pumpList; public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { } @@ -26,13 +26,9 @@ namespace Barotrauma protected override bool Filter(Pump pump) { - if (pump == null) { return false; } + if (pump?.Item == null || pump.Item.Removed) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } if (!pump.Item.IsInteractable(character)) { return false; } - if (pump.Item.HasTag("ballast")) { return false; } - if (pump.Item.Submarine == null) { return false; } - if (pump.Item.CurrentHull == null) { return false; } - if (pump.Item.Submarine.TeamID != character.TeamID) { return false; } if (pump.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } @@ -50,7 +46,16 @@ namespace Barotrauma if (pumpList == null) { if (character == null || character.Submarine == null) { return Array.Empty(); } - pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(p => p != null); + + pumpList = new List(); + foreach (Item item in character.Submarine.GetItems(true)) + { + var pump = item.GetComponent(); + if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; } + if (pump.Item.Submarine.TeamID != character.TeamID) { continue; } + if (pump.Item.HasTag("ballast")) { continue; } + pumpList.Add(pump); + } } return pumpList; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 27b75c722..ee2c985fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -238,7 +238,7 @@ namespace Barotrauma }; if (repairTool != null) { - objective.CloseEnough = repairTool.Range * 0.75f; + objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character); } return objective; }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 005e3aa44..90578f6b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -136,7 +136,7 @@ namespace Barotrauma return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1)); } - protected override IEnumerable GetList() => Item.ItemList; + protected override IEnumerable GetList() => Item.RepairableItems; protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem); @@ -156,6 +156,9 @@ namespace Barotrauma if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } if (item.Repairables.None()) { return false; } + + System.Diagnostics.Debug.Assert(item.Repairables.Any(), "Invalid target in AIObjectiveRepairItems - the objective should only be checking items that have a Repairable component (Item.RepairableItems)"); + return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 4be832fd0..c700ff0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using static Barotrauma.AIObjectiveFindSafety; namespace Barotrauma { @@ -186,7 +187,9 @@ namespace Barotrauma } else { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + HullSearchStatus hullSearchStatus = objectiveManager.GetObjective().FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls); + if (hullSearchStatus != HullSearchStatus.Finished) { return; } + safeHull = potentialSafeHull; findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index d9039091d..e4e900990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -1,8 +1,6 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using FarseerPhysics; -using Barotrauma.Extensions; namespace Barotrauma { @@ -90,6 +88,10 @@ namespace Barotrauma { steering = Vector2.Normalize(steering) * Math.Abs(speed); } + if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null) + { + steering = -steering; + } host.Steering = steering; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 92634f40a..9bb81acc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -85,7 +85,7 @@ namespace Barotrauma public readonly Identifier[] ForbiddenAmmunition; - public static WreckAIConfig GetRandom() => Prefabs.GetRandom(Rand.RandSync.ServerAndClient); + public static WreckAIConfig GetRandom() => Prefabs.OrderBy(p => p.UintIdentifier).GetRandom(Rand.RandSync.ServerAndClient); protected override Identifier DetermineIdentifier(XElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index ba7a674fb..258eb4e33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -24,7 +24,7 @@ namespace Barotrauma public bool IsAiming => wasAiming; public bool IsAimingMelee => wasAimingMelee; - protected bool Aiming => aiming || aimingMelee; + protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim); public float ArmLength => upperArmLength + forearmLength; @@ -275,6 +275,8 @@ namespace Barotrauma // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; + public float LockFlippingUntil; + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { useItemTimer = 0.5f; @@ -380,18 +382,10 @@ namespace Barotrauma { //if holding two items that should control the characters' pose, let the item in the right hand do it bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent()?.ControlPose ?? false); - if (!anotherItemControlsPose) + if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater) { - var head = GetLimb(LimbType.Head); - if (head != null) - { - head.body.SmoothRotate(itemAngle, force: 30 * head.Mass); - } - if (TargetMovement == Vector2.Zero && inWater) - { - torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; - torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); - } + torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; + torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); } aiming = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 81b328b03..f0818dd66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,8 +22,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - #warning TODO: this is kinda janky, this should probably be done better - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName)); + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); if (!character.VariantOf.IsEmpty) { _ragdollParams.ApplyVariantScale(character.Params.VariantFile); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 7953deff1..74f7ad622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -164,8 +164,6 @@ namespace Barotrauma public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale; public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale; - public float LockFlippingUntil; - public override Vector2 AimSourceSimPos { get @@ -841,7 +839,7 @@ namespace Barotrauma rotation += 360; } float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim)) + if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !Aiming) { if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 294285a87..bc2005a63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -74,7 +74,7 @@ namespace Barotrauma } } - public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count; + public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count; private bool frozen; public bool Frozen @@ -227,9 +227,13 @@ namespace Barotrauma { mainLimb = Limbs.FirstOrDefault(l => IsValid(l)); } + if (mainLimb == null) + { + DebugConsole.ThrowError("Couldn't find a valid main limb. The limb can't be hidden nor be set to ignore collisions!"); + mainLimb = Limbs.FirstOrDefault(); + } } - - bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; + static bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; return mainLimb; } } @@ -1850,36 +1854,30 @@ namespace Barotrauma } /// - /// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary. + /// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned. /// public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { - Limb limb = null; - if (HasMultipleLimbsOfSameType) + if (limbDictionary.TryGetValue(limbType, out Limb limb)) { - for (int i = 0; i < 10; i++) + if (excludeSevered && limb.IsSevered) { - limbDictionary.TryGetValue(limbType, out limb); - if (limb == null) + limb = null; + } + } + if (limb == null && HasMultipleLimbsOfSameType) + { + // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. + foreach (var l in limbs) + { + if (l.type != limbType) { continue; } + if (!excludeSevered || !l.IsSevered) { - // No limbs found - break; - } - if (!excludeSevered || !limb.IsSevered) - { - // Found a valid limb + limb = l; break; } } } - else - { - limbDictionary.TryGetValue(limbType, out limb); - } - if (excludeSevered && limb != null && limb.IsSevered) - { - limb = null; - } return limb; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 736e50d2c..e1d53f45d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -117,7 +117,29 @@ namespace Barotrauma protected Key[] keys; - public HumanPrefab HumanPrefab; + private HumanPrefab humanPrefab; + public HumanPrefab HumanPrefab + { + get { return humanPrefab; } + set + { + if (humanPrefab == value) { return; } + humanPrefab = value; + + if (humanPrefab != null) + { + HumanPrefabHealthMultiplier = humanPrefab.HealthMultiplier; + if (GameMain.NetworkMember != null) + { + HumanPrefabHealthMultiplier *= humanPrefab.HealthMultiplierInMultiplayer; + } + } + else + { + HumanPrefabHealthMultiplier = 1.0f; + } + } + } private CharacterTeamType teamID; public CharacterTeamType TeamID @@ -153,12 +175,12 @@ namespace Barotrauma protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; - public static void ThrowIfAccessingWalletsInSingleplayer() + private void ThrowIfAccessingWalletsInSingleplayer() { #if CLIENT && DEBUG if (Screen.Selected is TestScreen) { return; } #endif - if (GameMain.NetworkMember is null || GameMain.IsSingleplayer) + if ((GameMain.NetworkMember is null || GameMain.IsSingleplayer) && IsPlayer) { throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead."); } @@ -560,18 +582,35 @@ namespace Barotrauma #if CLIENT CharacterHealth.SetHealthBarVisibility(value == null); -#elif SERVER - if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) +#endif + bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) { - if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) +#if SERVER + if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) { - mpCampaign.Bank.Give(balance); + switch (settings.LootedMoneyDestination) + { + case LootedMoneyDestination.Wallet when IsPlayer: + Wallet.Give(balance); + break; + default: + mpCampaign.Bank.Give(balance); + break; + + } } - grabbedWallet.Deduct(balance); GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money); - } +#elif CLIENT + if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign) + { + spCampaign.Bank.Give(balance); + } #endif + + grabbedWallet.Deduct(balance); + } } } @@ -1175,7 +1214,7 @@ namespace Barotrauma CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } - if (Params.Husk && speciesName != "husk") + if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk") { // Get the non husked name and find the ragdoll with it var matchingAffliction = AfflictionPrefab.List @@ -1375,7 +1414,7 @@ namespace Barotrauma if (inputType == InputType.Up || inputType == InputType.Down || inputType == InputType.Left || inputType == InputType.Right) { - var invertControls = CharacterHealth.GetAffliction("invertcontrols"); + var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()); if (invertControls != null) { switch (inputType) @@ -1443,7 +1482,7 @@ namespace Barotrauma foreach (Item item in Inventory.AllItems) { - if (item?.Prefab.Identifier != "idcard") { continue; } + if (item?.GetComponent() == null) { continue; } foreach (string s in spawnPoint.IdCardTags) { item.AddTag(s); @@ -1635,14 +1674,9 @@ namespace Barotrauma } /// - /// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier + /// Health multiplier of the human prefab this character is an instance of (if any) /// - public float StaticHealthMultiplier { get; private set; } = 1; - - public void AddStaticHealthMultiplier(float newMultiplier) - { - StaticHealthMultiplier *= newMultiplier; - } + public float HumanPrefabHealthMultiplier { get; private set; } = 1; /// /// Speed reduction from the current limb specific damage. Min 0, max 1. @@ -3954,7 +3988,10 @@ namespace Barotrauma if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { // OnDamaged is called only for the limb that is hit. - AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); + foreach (Limb limb in AnimController.Limbs) + { + limb.ApplyStatusEffects(actionType, deltaTime); + } } //OnActive effects are handled by the afflictions themselves if (actionType != ActionType.OnActive) @@ -4804,21 +4841,21 @@ namespace Barotrauma } } - private readonly List abilityFlags = new List(); + private AbilityFlags abilityFlags; public void AddAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Add(abilityFlag); + abilityFlags |= abilityFlag; } public void RemoveAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Remove(abilityFlag); + abilityFlags &= ~abilityFlag; } public bool HasAbilityFlag(AbilityFlags abilityFlag) { - return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); + return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } private readonly Dictionary abilityResistances = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 61790a3b2..49d36bdfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -94,11 +94,67 @@ namespace Barotrauma public Vector2 SheetIndex => Preset.SheetIndex; - public ContentXElement HairElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairIndex); - public ContentXElement HairWithHatElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairWithHatIndex); - public ContentXElement BeardElement => CharacterInfo.Beards?.ElementAtOrDefault(BeardIndex); - public ContentXElement MoustacheElement => CharacterInfo.Moustaches?.ElementAtOrDefault(MoustacheIndex); - public ContentXElement FaceAttachment => CharacterInfo.FaceAttachments?.ElementAtOrDefault(FaceAttachmentIndex); + public ContentXElement HairElement + { + get + { + if (CharacterInfo.Hairs == null) { return null; } + if (hairIndex >= CharacterInfo.Hairs.Count) + { + DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})"); + } + return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex); + } + } + public ContentXElement HairWithHatElement + { + get + { + if (CharacterInfo.Hairs == null) { return null; } + if (HairWithHatIndex >= CharacterInfo.Hairs.Count) + { + DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})"); + } + return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex); + } + } + + public ContentXElement BeardElement + { + get + { + if (CharacterInfo.Beards == null) { return null; } + if (BeardIndex >= CharacterInfo.Beards.Count) + { + DebugConsole.AddWarning($"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})"); + } + return CharacterInfo.Beards.ElementAtOrDefault(BeardIndex); + } + } + public ContentXElement MoustacheElement + { + get + { + if (CharacterInfo.Moustaches == null) { return null; } + if (MoustacheIndex >= CharacterInfo.Moustaches.Count) + { + DebugConsole.AddWarning($"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})"); + } + return CharacterInfo.Moustaches.ElementAtOrDefault(MoustacheIndex); + } + } + public ContentXElement FaceAttachment + { + get + { + if (CharacterInfo.FaceAttachments == null) { return null; } + if (FaceAttachmentIndex >= CharacterInfo.FaceAttachments.Count) + { + DebugConsole.AddWarning($"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})"); + } + return CharacterInfo.FaceAttachments.ElementAtOrDefault(FaceAttachmentIndex); + } + } public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) { @@ -130,6 +186,10 @@ namespace Barotrauma head = value; HeadSprite = null; AttachmentSprites = null; + hairs = null; + beards = null; + moustaches = null; + faceAttachments = null; } } } @@ -252,12 +312,11 @@ namespace Barotrauma } /// - /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them + /// Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e.g. with an endocrine booster) /// - public IEnumerable GetEndocrineTalents() + public IEnumerable GetUnlockedTalentsOutsideTree() { if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } - return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t)); } @@ -297,7 +356,10 @@ namespace Barotrauma } } - public bool OmitJobInPortraitClothing; + /// + /// Can be used to disable displaying the job in any info panels + /// + public bool OmitJobInMenus; private Sprite portrait; public Sprite Portrait @@ -375,7 +437,7 @@ namespace Barotrauma { if (attachmentSprites == null) { - LoadAttachmentSprites(OmitJobInPortraitClothing); + LoadAttachmentSprites(); } return attachmentSprites; } @@ -844,7 +906,14 @@ namespace Barotrauma public void RecreateHead(ImmutableHashSet tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags)); - if (headPreset == null) { headPreset = Prefab.Heads.GetRandomUnsynced(); } + if (headPreset == null) + { + if (tags.Count == 1) + { + headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First())); + } + headPreset ??= Prefab.Heads.GetRandomUnsynced(); + } head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ReloadHeadAttachments(); } @@ -1026,7 +1095,7 @@ namespace Barotrauma private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); - partial void LoadAttachmentSprites(bool omitJob); + partial void LoadAttachmentSprites(); private int CalculateSalary() { @@ -1182,7 +1251,7 @@ namespace Barotrauma // Replace the name tag of any existing id cards or duffel bags foreach (var item in Item.ItemList) { - if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; } + if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; } foreach (var tag in item.Tags.Split(',')) { var splitTag = tag.Split(":"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 93fe641e6..c8dca9919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -35,6 +35,7 @@ namespace Barotrauma if (newValue > _strength) { PendingAdditionStrength = Prefab.GrainBurst; + Duration = Prefab.Duration; } _strength = newValue; } @@ -60,6 +61,8 @@ namespace Barotrauma public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + public float Duration; + /// /// Which character gave this affliction /// @@ -75,6 +78,8 @@ namespace Barotrauma _strength = strength; Identifier = prefab.Identifier; + Duration = prefab.Duration; + foreach (var periodicEffect in prefab.PeriodicEffects) { PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); @@ -315,8 +320,7 @@ namespace Barotrauma public bool HasFlag(AbilityFlags flagType) { if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } - - return currentEffect.AfflictionAbilityFlags.Contains(flagType); + return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } private AfflictionPrefab.Effect GetViableEffect() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 4bc5d873b..04d620a18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -235,7 +235,7 @@ namespace Barotrauma public Identifier[] BlockTransformation { get; private set; } public readonly Dictionary AfflictionStatValues = new Dictionary(); - public readonly HashSet AfflictionAbilityFlags = new HashSet(); + public AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); @@ -265,7 +265,7 @@ namespace Barotrauma break; case "abilityflag": var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); - AfflictionAbilityFlags.Add(flagType); + AfflictionAbilityFlags |= flagType; break; case "affliction": DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); @@ -300,6 +300,7 @@ namespace Barotrauma } public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; + public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; public static AfflictionPrefab Burn => Prefabs["burn"]; @@ -353,6 +354,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// The affliction is automatically removed after this time. 0 = unlimited + /// + public readonly float Duration; + //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; @@ -406,8 +412,10 @@ namespace Barotrauma !IsBuff && AfflictionType != "geneticmaterialbuff" && AfflictionType != "geneticmaterialdebuff"); - HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); - BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); + HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); + BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 7687f0b65..bdd89998c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,12 +1,11 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using System.Globalization; -using Barotrauma.Abilities; namespace Barotrauma { @@ -124,7 +123,18 @@ namespace Barotrauma public float PressureKillDelay { get; private set; } = 5.0f; - public float Vitality { get; private set; } + private float vitality; + public float Vitality + { + get + { + return Character.IsDead ? minVitality : vitality; + } + private set + { + vitality = value; + } + } public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality); @@ -137,7 +147,7 @@ namespace Barotrauma { max += Character.Info.Job.Prefab.VitalityModifier; } - max *= Character.StaticHealthMultiplier; + max *= Character.HumanPrefabHealthMultiplier; max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } @@ -689,6 +699,7 @@ namespace Barotrauma newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } existingAffliction.Strength = newStrength; + existingAffliction.Duration = existingAffliction.Prefab.Duration; if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); if (Vitality <= MinVitality) @@ -725,6 +736,8 @@ namespace Barotrauma AddLimbAffliction(limbHealth: null, newAffliction, allowStacking); } + partial void UpdateSkinTint(); + partial void UpdateLimbAfflictionOverlays(); public void Update(float deltaTime) @@ -746,6 +759,15 @@ namespace Barotrauma if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } continue; } + if (affliction.Prefab.Duration > 0.0f) + { + affliction.Duration -= deltaTime; + if (affliction.Duration <= 0.0f) + { + afflictionsToRemove.Add(affliction); + continue; + } + } afflictionsToUpdate.Add(kvp); } foreach (KeyValuePair kvp in afflictionsToUpdate) @@ -788,7 +810,7 @@ namespace Barotrauma if (!Character.GodMode) { UpdateLimbAfflictionOverlays(); - UpdateSkinTint(); + UpdateSkinTint(); CalculateVitality(); if (Vitality <= MinVitality) @@ -798,23 +820,6 @@ namespace Barotrauma } } - private void UpdateSkinTint() - { - FaceTint = DefaultFaceTint; - BodyTint = Color.TransparentBlack; - - if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } - - foreach (KeyValuePair kvp in afflictions) - { - var affliction = kvp.Key; - Color faceTint = affliction.GetFaceTint(); - if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } - Color bodyTint = affliction.GetBodyTint(); - if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } - } - } - private void UpdateDamageReductions(float deltaTime) { float healthRegen = Character.Params.Health.ConstantHealthRegeneration; @@ -905,6 +910,7 @@ namespace Barotrauma if (Unkillable || Character.GodMode) { return; } var (type, affliction) = GetCauseOfDeath(); + UpdateLimbAfflictionOverlays(); UpdateSkinTint(); Character.Kill(type, affliction); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 46a7da94f..7e8a539ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -105,19 +105,13 @@ namespace Barotrauma return spawnPointTags; } - public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced) + public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced, Func predicate = null) { - return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync); + return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate); } public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) { - npc.AddStaticHealthMultiplier(HealthMultiplier); - if (GameMain.NetworkMember != null) - { - npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer); - } - var humanAI = npc.AIController as HumanAIController; if (humanAI != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index f9f45d936..615a33350 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -209,11 +209,8 @@ namespace Barotrauma } } - if (item.Prefab.Identifier == "idcard") - { - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(spawnPoint, character); - } + IdCard idCardComponent = item.GetComponent(); + idCardComponent?.Initialize(spawnPoint, character); foreach (WifiComponent wifiComponent in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 1d1c112f1..8153bca07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -79,7 +79,6 @@ namespace Barotrauma /// public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; - public static ContentXElement NoJobElement; public static JobPrefab Get(string identifier) { if (Prefabs.ContainsKey(identifier)) @@ -213,7 +212,7 @@ namespace Barotrauma public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill); public ContentXElement Element { get; private set; } - public ContentXElement ClothingElement { get; private set; } + public int Variants { get; private set; } public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) @@ -288,11 +287,8 @@ namespace Barotrauma Variants = variant; Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); - - // Disabled on purpose, TODO: remove all references? - //ClothingElement = element.GetChildElement("PortraitClothing"); } - public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync); + public static JobPrefab Random(Rand.RandSync sync, Func predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index f9c0f5562..e958f391f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -331,7 +331,7 @@ namespace Barotrauma #if CLIENT if (isSevered) { - damageOverlayStrength = 100.0f; + damageOverlayStrength = 1.0f; } #endif } @@ -352,7 +352,7 @@ namespace Barotrauma public Vector2 Position { - get { return ConvertUnits.ToDisplayUnits(body.SimPosition); } + get { return ConvertUnits.ToDisplayUnits(body?.SimPosition ?? Vector2.Zero); } } public Vector2 SimPosition @@ -597,18 +597,7 @@ namespace Barotrauma dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; - if (limbParams.IgnoreCollisions) - { - body.CollisionCategories = Category.None; - body.CollidesWith = Category.None; - IgnoreCollisions = true; - } - else - { - //limbs don't collide with each other - body.CollisionCategories = Physics.CollisionCharacter; - body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; - } + IgnoreCollisions = limbParams.IgnoreCollisions; body.UserData = this; pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale)) { @@ -646,10 +635,9 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (!character.VariantOf.IsEmpty) + if (character is { VariantOf: { IsEmpty: false } }) { - var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab) - ? basePrefab.ConfigElement.GetChildElement("attack") : null; + var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 967927105..5b45d8882 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -574,7 +574,7 @@ namespace Barotrauma public float AggressionGreed { get; private set; } [Serialize(0f, IsPropertySaveable.Yes, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] - public float FleeHealthThreshold { get; private set; } + public float FleeHealthThreshold { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] public bool AttackWhenProvoked { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 608d7f16f..d8f8992b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -121,7 +121,7 @@ namespace Barotrauma return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); + public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName); /// /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! @@ -138,61 +138,53 @@ namespace Barotrauma ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) { return (T)ragdoll; } - string selectedFile = null; - - void tryFolderForSpecies(Identifier species, out string err) + Identifier ragdollSpecies = speciesName; + if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)) { - err = null; - string folder = GetFolder(species); + if (!prefab.VariantOf.IsEmpty) + { + ragdollSpecies = prefab.VariantOf; + } + string error = null; + string folder = GetFolder(ragdollSpecies); if (!Directory.Exists(folder)) { - err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - return; - } - - string[] files = Directory.GetFiles(folder); - if (files.None()) - { - err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(species); + error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); } else { - selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); - if (selectedFile == null) + string[] files = Directory.GetFiles(folder); + if (files.None()) { - err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); + error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); + } + else if (string.IsNullOrEmpty(fileName)) + { + // Files found, but none specified + selectedFile = GetDefaultFile(ragdollSpecies); + } + else + { + selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + if (selectedFile == null) + { + error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); + } } } + if (error != null) + { + DebugConsole.ThrowError(error); + } } - - tryFolderForSpecies(speciesName, out var error); - Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab) - ? prefab.VariantOf - : Identifier.Empty; - if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty) - { - tryFolderForSpecies(parentSpeciesName, out error); - } - - if (!error.IsNullOrEmpty()) - { - DebugConsole.ThrowError(error); - } - if (selectedFile == null) { throw new Exception("[RagdollParams] Selected file null!"); @@ -200,7 +192,7 @@ namespace Barotrauma DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); var characterPrefab = CharacterPrefab.Prefabs[speciesName]; T r = new T(); - if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) + if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies)) { if (!ragdolls.ContainsKey(r.Name)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index 7065cb683..426156bec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs index a1e03fcb6..782856d1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { abstract class AbilityConditionDataless : AbilityCondition { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 4c29a5b61..044d960a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 70ec6e1ae..5711c0ed5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 3f9090376..70b871963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,7 +1,4 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -19,10 +16,9 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - IEnumerable chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character); - - foreach (Character character in chosenCharacters) + foreach (Character character in Character.GetFriendlyCrew(Character)) { + if (!allowSelf && character == Character) { continue; } if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs index d6fc8b329..332c92e20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs index 4594c5e1e..693924271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index f8329aae2..94f7dfe02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 1f3795dea..656e5d751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index a12e2ce1e..b77f4332d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { @@ -30,7 +24,14 @@ namespace Barotrauma.Abilities private bool IsApplicable(AbilityObject abilityObject) { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition(abilityObject)); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition(abilityObject)) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 8682a47df..7cc1e24eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -49,7 +43,14 @@ namespace Barotrauma.Abilities private bool IsApplicable() { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition()); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition()) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index bfa5b6869..0cf4b3420 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -1,8 +1,6 @@ -using System; +using Barotrauma.Abilities; +using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs index 1b45bc400..eb564c001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior"; protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors"; - protected override PrefabCollection prefabs => BallastFloraPrefab.Prefabs; + protected override PrefabCollection Prefabs => BallastFloraPrefab.Prefabs; protected override BallastFloraPrefab CreatePrefab(ContentXElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs index 717057ff6..afb3296aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "cave"; protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters"; - protected override PrefabCollection prefabs => CaveGenerationParams.CaveParams; + protected override PrefabCollection Prefabs => CaveGenerationParams.CaveParams; protected override CaveGenerationParams CreatePrefab(ContentXElement element) { return new CaveGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index 1a0b569d5..e3412c1be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -77,7 +77,7 @@ namespace Barotrauma { HashSet texturePaths = new HashSet { - ragdollParams.Texture + ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value }; foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 4f853ba01..736f5053e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -2,12 +2,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -69,10 +67,10 @@ namespace Barotrauma .ToImmutableHashSet(); } - public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error, string? stackTrace = null) - => Result.Failure(error, stackTrace); + static Result fail(string error, Exception? exception = null) + => Result.Failure(new LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -95,11 +93,11 @@ namespace Barotrauma var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") - : Result.Success(file); + : Result.Success(file); } catch (Exception e) { - return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace()); + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e); } } @@ -125,5 +123,23 @@ namespace Barotrauma } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); + + public readonly struct LoadError + { + public readonly string Message; + public readonly Exception? Exception; + + public LoadError(string message, Exception? exception) + { + Message = message; + Exception = exception; + } + + public override string ToString() + => Message + + (Exception is { StackTrace: var stackTrace } + ? '\n' + stackTrace.CleanupStackTrace() + : string.Empty); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs index b9eb4ddce..79e071b9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse"; protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses"; - protected override PrefabCollection prefabs => CorpsePrefab.Prefabs; + protected override PrefabCollection Prefabs => CorpsePrefab.Prefabs; protected override CorpsePrefab CreatePrefab(ContentXElement element) { return new CorpsePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs index 298f618c5..0e6c4c309 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs @@ -8,7 +8,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings"; - protected override PrefabCollection prefabs => EventManagerSettings.Prefabs; + protected override PrefabCollection Prefabs => EventManagerSettings.Prefabs; protected override EventManagerSettings CreatePrefab(ContentXElement element) { return new EventManagerSettings(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs index bb200e5c6..fac0ae4cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "faction"; protected override bool MatchesPlural(Identifier identifier) => identifier == "factions"; - protected override PrefabCollection prefabs => FactionPrefab.Prefabs; + protected override PrefabCollection Prefabs => FactionPrefab.Prefabs; protected override FactionPrefab CreatePrefab(ContentXElement element) { return new FactionPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index e59272553..cd0906887 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected abstract bool MatchesSingular(Identifier identifier); protected abstract bool MatchesPlural(Identifier identifier); - protected abstract PrefabCollection prefabs { get; } + protected abstract PrefabCollection Prefabs { get; } protected abstract T CreatePrefab(ContentXElement element); private void LoadFromXElement(ContentXElement parentElement, bool overriding) @@ -29,14 +29,14 @@ namespace Barotrauma } else if (elemName == "clear") { - prefabs.AddOverrideFile(this); + Prefabs.AddOverrideFile(this); } else if (MatchesSingular(elemName)) { T prefab = CreatePrefab(parentElement); try { - prefabs.Add(prefab, overriding); + Prefabs.Add(prefab, overriding); } catch { @@ -53,7 +53,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } @@ -68,12 +68,12 @@ namespace Barotrauma public override sealed void UnloadFile() { - prefabs.RemoveByFile(this); + Prefabs.RemoveByFile(this); } public sealed override void Sort() { - prefabs.SortAll(); + Prefabs.SortAll(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs index bff61b1bb..4186f99d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly"; protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies"; - protected override PrefabCollection prefabs => ItemAssemblyPrefab.Prefabs; + protected override PrefabCollection Prefabs => ItemAssemblyPrefab.Prefabs; protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element) { return new ItemAssemblyPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs index 5065470c2..afb002440 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "items"; - protected override PrefabCollection prefabs => ItemPrefab.Prefabs; + protected override PrefabCollection Prefabs => ItemPrefab.Prefabs; protected override ItemPrefab CreatePrefab(ContentXElement element) { return new ItemPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs index 7c2291e84..3fa28a615 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs @@ -22,11 +22,7 @@ namespace Barotrauma { foreach (var element in mainElement.Elements()) { - if (element.NameAsIdentifier() == "nojob") - { - JobPrefab.NoJobElement ??= element; - } - else if (element.NameAsIdentifier() == "ItemRepairPriorities") + if (element.NameAsIdentifier() == "ItemRepairPriorities") { foreach (var subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs index 228dca7cf..2f359a366 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects"; - protected override PrefabCollection prefabs => LevelObjectPrefab.Prefabs; + protected override PrefabCollection Prefabs => LevelObjectPrefab.Prefabs; protected override LevelObjectPrefab CreatePrefab(ContentXElement element) { return new LevelObjectPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs index cd3cc4c91..f9376f752 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes"; - protected override PrefabCollection prefabs => LocationType.Prefabs; + protected override PrefabCollection Prefabs => LocationType.Prefabs; protected override LocationType CreatePrefab(ContentXElement element) { return new LocationType(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs index 11efb2d0c..8907524de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs @@ -23,7 +23,7 @@ namespace Barotrauma /*missionTypes.Any(t => identifier == t.Name) || identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/ protected override bool MatchesPlural(Identifier identifier) => identifier == "missions"; - protected override PrefabCollection prefabs => MissionPrefab.Prefabs; + protected override PrefabCollection Prefabs => MissionPrefab.Prefabs; protected override MissionPrefab CreatePrefab(ContentXElement element) { return new MissionPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs index 85b2548d9..4433b6158 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset"; protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets"; - protected override PrefabCollection prefabs => NPCSet.Sets; + protected override PrefabCollection Prefabs => NPCSet.Sets; protected override NPCSet CreatePrefab(ContentXElement element) { return new NPCSet(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs index 5699a0410..57273ced6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -42,7 +42,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs index 1972243dc..1e2546859 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters"; - protected override PrefabCollection prefabs => OutpostGenerationParams.OutpostParams; + protected override PrefabCollection Prefabs => OutpostGenerationParams.OutpostParams; protected override OutpostGenerationParams CreatePrefab(ContentXElement element) { return new OutpostGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs index de128d17c..3730156ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles"; - protected override PrefabCollection prefabs => ParticlePrefab.Prefabs; + protected override PrefabCollection Prefabs => ParticlePrefab.Prefabs; protected override ParticlePrefab CreatePrefab(ContentXElement element) { return new ParticlePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index d4c5b1c43..6f636c4e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -57,7 +57,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs index 9a8de8fd1..08ba4d436 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs @@ -10,7 +10,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters"; - protected override PrefabCollection prefabs => RuinGenerationParams.RuinParams; + protected override PrefabCollection Prefabs => RuinGenerationParams.RuinParams; protected override RuinGenerationParams CreatePrefab(ContentXElement element) { return new RuinGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs index 57034f4d1..b14263066 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } - protected override PrefabCollection prefabs => SoundPrefab.Prefabs; + protected override PrefabCollection Prefabs => SoundPrefab.Prefabs; protected override SoundPrefab CreatePrefab(ContentXElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs new file mode 100644 index 000000000..072acfe63 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs @@ -0,0 +1,12 @@ +namespace Barotrauma +{ + sealed class StartItemsFile : GenericPrefabFile + { + public StartItemsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "itemset"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "startitems"; + protected override PrefabCollection Prefabs => StartItemSet.Sets; + protected override StartItemSet CreatePrefab(ContentXElement element) => new StartItemSet(element, this); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs index b961311ab..bec8357b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures"; - protected override PrefabCollection prefabs => StructurePrefab.Prefabs; + protected override PrefabCollection Prefabs => StructurePrefab.Prefabs; protected override StructurePrefab CreatePrefab(ContentXElement element) { return new StructurePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs index 6ca1f9c68..cc25d8fc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree"; protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees"; - protected override PrefabCollection prefabs => TalentTree.JobTalentTrees; + protected override PrefabCollection Prefabs => TalentTree.JobTalentTrees; protected override TalentTree CreatePrefab(ContentXElement element) { return new TalentTree(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs index 1b5b05f4f..c234bd117 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "talent"; protected override bool MatchesPlural(Identifier identifier) => identifier == "talents"; - protected override PrefabCollection prefabs => TalentPrefab.TalentPrefabs; + protected override PrefabCollection Prefabs => TalentPrefab.TalentPrefabs; protected override TalentPrefab CreatePrefab(ContentXElement element) { return new TalentPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs index 3a58364e0..a43c10379 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs @@ -17,7 +17,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; - protected override PrefabCollection prefabs => PrefabType.Prefabs; + protected override PrefabCollection Prefabs => PrefabType.Prefabs; protected override PrefabType CreatePrefab(ContentXElement element) { return new PrefabType(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs index 61de9e96b..91e1a8d47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected override bool MatchesPlural(Identifier identifier) => identifier == "upgrademodules"; - protected override PrefabCollection prefabs => UpgradeContentPrefab.PrefabsAndCategories; + protected override PrefabCollection Prefabs => UpgradeContentPrefab.PrefabsAndCategories; protected override UpgradeContentPrefab CreatePrefab(ContentXElement element) { Identifier elemName = element.NameAsIdentifier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs index 54a445ff0..be1c2ef5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs"; - protected override PrefabCollection prefabs => WreckAIConfig.Prefabs; + protected override PrefabCollection Prefabs => WreckAIConfig.Prefabs; protected override WreckAIConfig CreatePrefab(ContentXElement element) { return new WreckAIConfig(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index eba749c54..bcb068006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -14,8 +14,7 @@ namespace Barotrauma { public abstract class ContentPackage { - #warning TODO: make this independent of the current version - public static readonly Version MinimumHashCompatibleVersion = GameMain.Version; + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 3, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -34,11 +33,11 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; - public readonly Md5Hash Hash; + public Md5Hash Hash { get; private set; } public readonly DateTime? InstallTime; - public readonly ImmutableArray Files; - public readonly ImmutableArray<(string error, string? stackTrace)> Errors; + public ImmutableArray Files { get; private set; } + public ImmutableArray Errors { get; private set; } public async Task IsUpToDate() { @@ -56,7 +55,7 @@ namespace Barotrauma /// /// Does the content package include some content that needs to match between all players in multiplayer. /// - public readonly bool HasMultiplayerSyncedContent; + public bool HasMultiplayerSyncedContent { get; private set; } protected ContentPackage(XDocument doc, string path) { @@ -85,13 +84,13 @@ namespace Barotrauma .ToArray(); Files = fileResults - .OfType>() + .OfType>() .Select(f => f.Value) .ToImmutableArray(); Errors = fileResults - .OfType>() - .Select(f => (f.Error, f.StackTrace)) + .OfType>() + .Select(f => f.Error) .ToImmutableArray(); HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); @@ -128,18 +127,13 @@ namespace Barotrauma try { - if (doc.Root.GetAttributeBool("corepackage", false)) - { - return new CorePackage(doc, path); - } - else - { - return new RegularPackage(doc, path); - } + return doc.Root.GetAttributeBool("corepackage", false) + ? (ContentPackage)new CorePackage(doc, path) + : new RegularPackage(doc, path); } catch (Exception e) { - while (e.InnerException != null) { e = e.InnerException; } + e = e.GetInnermost(); DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); return null; } @@ -279,12 +273,42 @@ namespace Barotrauma Files.ForEach(f => f.UnloadFile()); } - public override int GetHashCode() + public void ReloadSubsAndItemAssemblies() { - byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4)); - return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3]; + XDocument doc = XMLExtensions.TryLoadXml(Path); + List newFileList = new List(); + XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); + + var fileResults = rootElement.Elements() + .Select(e => ContentFile.CreateFromXElement(this, e)) + .ToArray(); + + foreach (var result in fileResults) + { + switch (result) + { + case Success { Value: var file }: + if (file is BaseSubFile || file is ItemAssemblyFile) + { + newFileList.Add(file); + } + else + { + var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); + newFileList.Add(existingFile ?? file); + } + break; + } + } + + UnloadFilesOfType(); + UnloadFilesOfType(); + Files = newFileList.ToImmutableArray(); + Hash = CalculateHash(); + LoadFilesOfType(); + LoadFilesOfType(); } - + public static bool PathAllowedAsLocalModFile(string path) { #if DEBUG @@ -306,21 +330,17 @@ namespace Barotrauma public void LogErrors() { - if (Errors.Any()) + if (!Errors.Any()) { - DebugConsole.AddWarning( - $"The following errors occurred while loading the content package\"{Name}\". The package might not work correctly.\n" + - string.Join('\n', Errors.Select(e => errorToStr(e.error, e.stackTrace)))); - static string errorToStr(string error, string? stackTrace) - { - string str = error; - if (stackTrace != null) - { - str += '\n' + stackTrace; - } - return str; - } + return; } + + DebugConsole.AddWarning( + $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + + string.Join('\n', Errors.Select(errorToStr))); + + static string errorToStr(ContentFile.LoadError error) + => error.ToString(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 68a695516..1d11503a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -430,9 +430,9 @@ namespace Barotrauma public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); - foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors) + foreach (ContentFile.LoadError error in VanillaCorePackage.Errors) { - DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace)); + DebugConsole.ThrowError(error.ToString()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index d105e09cc..b7388bb2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -49,7 +49,10 @@ namespace Barotrauma .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); } } - var allPackages = ContentPackageManager.EnabledPackages.All; + var allPackages = ContentPackageManager.AllPackages; +#if CLIENT + if (GameMain.ModDownloadScreen?.DownloadedPackages != null) { allPackages = allPackages.Concat(GameMain.ModDownloadScreen.DownloadedPackages); } +#endif foreach (Identifier otherModName in otherMods) { if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index f5dba0b3d..b184da625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -27,7 +27,7 @@ namespace Barotrauma public string BaseUri => Element.BaseUri; - public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null."); + public XDocument? Document => Element.Document; public ContentXElement? FirstElement() => Elements().FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs index f6fb91198..2c6d0df5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs @@ -11,7 +11,7 @@ namespace Barotrauma { Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " + $"with name or ID \"{missingPackage ?? "[NULL]"}\" " + - $"that is not currently enabled."; + $"that is not currently installed."; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index afa6c9758..0b4ac4c1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -780,7 +780,7 @@ namespace Barotrauma return; } GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(true); + newEvent.Init(); NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); return; } @@ -1122,7 +1122,7 @@ namespace Barotrauma { var gamesession = new GameSession( SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), - GameModePreset.DevSandbox); + GameModePreset.DevSandbox ?? GameModePreset.Sandbox); string seed = ToolBox.RandomSeed(16); gamesession.StartRound(seed); @@ -1829,6 +1829,17 @@ namespace Barotrauma })); #endif + commands.Add(new Command("startitems|startitemset", "start item set identifier", (string[] args) => + { + if (args.Length == 0) + { + ThrowError($"No start item set identifier defined!"); + return; + } + AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier(); + NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\""); + }, isCheat: false)); + //"dummy commands" that only exist so that the server can give clients permissions to use them //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => @@ -2515,5 +2526,15 @@ namespace Barotrauma ThrowError("Saving debug console log to " + filePath + " failed", e); } } + + public static void DeactivateCheats() + { +#if CLIENT + GameMain.DebugDraw = false; + GameMain.LightManager.LightingEnabled = true; +#endif + Hull.EditWater = false; + Hull.EditFire = false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 94123f36e..bf8d1addd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -33,13 +33,13 @@ namespace Barotrauma public enum AbilityEffectType { Undefined, - None, + None, OnAttack, OnAttackResult, OnAttacked, OnAttackedResult, - OnGainSkillPoint, - OnAllyGainSkillPoint, + OnGainSkillPoint, + OnAllyGainSkillPoint, OnRepairComplete, OnItemFabricationSkillGain, OnItemFabricatedAmount, @@ -131,21 +131,22 @@ namespace Barotrauma MaxAttachableCount, } + [Flags] public enum AbilityFlags { - None, - MustWalk, - ImmuneToPressure, - IgnoredByEnemyAI, - MoveNormallyWhileDragging, - CanTinker, - CanTinkerFabricatorsAndDeconstructors, - TinkeringPowersDevices, - GainSkillPastMaximum, - RetainExperienceForNewCharacter, - AllowSecondOrderedTarget, - PowerfulCPR, - AlwaysStayConscious, + None = 0, + MustWalk = 0x1, + ImmuneToPressure = 0x2, + IgnoredByEnemyAI = 0x4, + MoveNormallyWhileDragging = 0x8, + CanTinker = 0x10, + CanTinkerFabricatorsAndDeconstructors = 0x20, + TinkeringPowersDevices = 0x40, + GainSkillPastMaximum = 0x80, + RetainExperienceForNewCharacter = 0x100, + AllowSecondOrderedTarget = 0x200, + PowerfulCPR = 0x400, + AlwaysStayConscious = 0x800, } [Flags] @@ -155,4 +156,32 @@ namespace Barotrauma Player = 0b10, Both = Bot | Player } + + public enum StartingBalanceAmount + { + Low, + Medium, + High, + } + + public enum GameDifficulty + { + Easy, + Medium, + Hard, + Hellish + } + + public enum NumberType + { + Int, + Float + } + + public enum ChatMode + { + None, + Local, + Radio + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 3c8e23fd4..2275bcd06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -53,8 +53,9 @@ namespace Barotrauma } } - public override void Init(bool affectSubImmediately) + public override void Init(EventSet parentSet) { + base.Init(parentSet); spawnPos = Level.Loaded.GetRandomItemPos( (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : @@ -111,7 +112,7 @@ namespace Barotrauma case 1: if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; - Finished(); + Finish(); state = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index be1857c50..192664d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -5,13 +5,16 @@ using System.Collections.Generic; namespace Barotrauma { class Event - { + { + public event Action Finished; protected bool isFinished; protected readonly EventPrefab prefab; public EventPrefab Prefab => prefab; + public EventSet ParentSet { get; private set; } + public Func SpawnPosFilter; public bool IsFinished @@ -42,23 +45,20 @@ namespace Barotrauma yield break; } - public virtual void Init(bool affectSubImmediately) + public virtual void Init(EventSet parentSet = null) { + ParentSet = parentSet; } public virtual void Update(float deltaTime) { } - public virtual void Finished() + public virtual void Finish() { isFinished = true; - } - - public virtual bool CanAffectSubImmediately(Level level) - { - return true; - } + Finished?.Invoke(); + } public virtual bool LevelMeetsRequirements() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index c3b9fad93..27748f54d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -313,10 +313,14 @@ namespace Barotrauma bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && (e == Character.Controlled || character.IsRemotePlayer); #if SERVER - UpdateIgnoredClients(); - isValid &= !ignoredClients.Keys.Any(c => c.Character == e); + if (!dialogOpened) + { + UpdateIgnoredClients(); + isValid &= !ignoredClients.Keys.Any(c => c.Character == e); + } #elif CLIENT - isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen); + bool block = GUI.InputBlockingMenuOpen && !dialogOpened; + isValid &= (e != Character.Controlled || !block); #endif return isValid; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index da7661209..d4d95ebbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -80,7 +80,7 @@ namespace Barotrauma } if (campaign is MultiPlayerCampaign mpCampaign) { - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); } if (prefab != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index c871e45ab..c3030d6df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -64,8 +64,6 @@ namespace Barotrauma campaign.GetWallet(client).Give(Amount); } } - - ((MultiPlayerCampaign)campaign).LastUpdateID++; #else campaign.Wallet.Give(Amount); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index a05b0a183..70f6a7b61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -49,6 +49,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier SpawnPointTag { get; set; } + [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] + public CharacterTeamType Team { get; protected set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] + public bool RequireSpawnPointTag { get; set; } + private readonly HashSet targetModuleTags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] @@ -79,7 +85,7 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase)); + ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; } public override bool IsFinished(ref string goTo) @@ -110,22 +116,40 @@ namespace Barotrauma if (humanPrefab != null) { ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + if (spawnPos != null) { - if (newCharacter == null) { return; } - newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); - if (LootingIsStealing) + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.AllItems) + if (newCharacter == null) { return; } + newCharacter.HumanPrefab = humanPrefab; + newCharacter.TeamID = Team; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + if (LootingIsStealing) { - item.SpawnedInCurrentOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.AllItems) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!TargetTag.IsEmpty && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } + } + } + else if (!SpeciesName.IsEmpty) + { + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter => + { if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); @@ -134,20 +158,9 @@ namespace Barotrauma }); } } - else if (!SpeciesName.IsEmpty) - { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => - { - if (!TargetTag.IsEmpty && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); - } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab)) + if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -178,7 +191,11 @@ namespace Barotrauma if (spawnInventory == null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned); + } } else { @@ -244,10 +261,10 @@ namespace Barotrauma SpawnType? spawnPointType = null; if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; } - return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); + return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { List potentialSpawnPoints = spawnLocation switch { @@ -274,18 +291,24 @@ namespace Barotrauma if (spawnpointTags != null && spawnpointTags.Any()) { var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag))) - .Where(wp => wp.ConnectedDoor == null && !wp.isObstructed); + .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - if (spawnPoints.Any()) + if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.Count == 0) + if (potentialSpawnPoints.None()) { - DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) + { + DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White); + } + else + { + DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + } return null; } @@ -307,7 +330,7 @@ namespace Barotrauma validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); } - if (!validSpawnPoints.Any()) + if (validSpawnPoints.None()) { DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})"); return potentialSpawnPoints.GetRandomUnsynced(); @@ -320,7 +343,7 @@ namespace Barotrauma } //if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags - if (spawnpointTags == null || !spawnpointTags.Any()) + if (spawnpointTags == null || spawnpointTags.None()) { var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any()); if (spawnPoints.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 8bbb511bf..63dd365ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -117,6 +117,8 @@ namespace Barotrauma public bool Enabled = true; + private MTRandom rand; + public void StartRound(Level level) { this.level = level; @@ -147,7 +149,7 @@ namespace Barotrauma seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); } } - MTRandom rand = new MTRandom(seed); + rand = new MTRandom(seed); EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); EventSet additiveSet = null; @@ -159,12 +161,12 @@ namespace Barotrauma if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); - CreateEvents(initialEventSet, rand); + CreateEvents(initialEventSet); } if (additiveSet != null) { pendingEventSets.Add(additiveSet); - CreateEvents(additiveSet, rand); + CreateEvents(additiveSet); } if (level?.LevelData?.Type == LevelData.LevelType.Outpost) @@ -183,7 +185,7 @@ namespace Barotrauma if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); - newEvent.Init(true); + newEvent.Init(); ActiveEvents.Add(newEvent); } else @@ -258,9 +260,15 @@ namespace Barotrauma throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } + float extraDifficulty = 0; + if (GameMain.GameSession.Campaign?.Settings != null) + { + extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty; + } + float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100); var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => - level.Difficulty >= s.MinLevelDifficulty && - level.Difficulty <= s.MaxLevelDifficulty).ToArray(); + modifiedDifficulty >= s.MinLevelDifficulty && + modifiedDifficulty <= s.MaxLevelDifficulty).ToArray(); if (suitableSettings.Length == 0) { @@ -362,8 +370,9 @@ namespace Barotrauma return retVal; } - private void CreateEvents(EventSet eventSet, Random rand) + private void CreateEvents(EventSet eventSet) { + selectedEvents.Remove(eventSet); if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); @@ -399,6 +408,14 @@ namespace Barotrauma bool isPrefabSuitable(EventPrefab e) => e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + + foreach (var subEventPrefab in eventSet.EventPrefabs) + { + foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) + { + DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); + } + } var suitablePrefabSubsets = eventSet.EventPrefabs.Where( e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); @@ -421,7 +438,7 @@ namespace Barotrauma var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(true); + newEvent.Init(eventSet); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -438,7 +455,7 @@ namespace Barotrauma var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); if (newEventSet != null) { - CreateEvents(newEventSet, rand); + CreateEvents(newEventSet); } } } @@ -451,7 +468,7 @@ namespace Barotrauma var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(true); + newEvent.Init(eventSet); DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { @@ -465,7 +482,7 @@ namespace Barotrauma { if (!IsValidForLevel(childEventSet, level)) { continue; } if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } - CreateEvents(childEventSet, rand); + CreateEvents(childEventSet); } } } @@ -666,6 +683,14 @@ namespace Barotrauma { eventCoolDown = settings.EventCooldown; } + if (eventSet.ResetTime > 0) + { + ev.Finished += () => + { + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); + }; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 150aee8b6..3c2be4201 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -58,7 +58,7 @@ namespace Barotrauma } #endif - public static List GetAllEventPrefabs() + public static List GetAllEventPrefabs() { List eventPrefabs = EventPrefab.Prefabs.ToList(); foreach (var eventSet in Prefabs) @@ -118,6 +118,8 @@ namespace Barotrauma public readonly float DefaultCommonness; public readonly ImmutableDictionary OverrideCommonness; + public readonly float ResetTime; + public readonly struct SubEventPrefab { public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) @@ -140,11 +142,15 @@ namespace Barotrauma { foreach (var id in (Identifier[])PrefabOrIdentifier) { - yield return EventPrefab.Prefabs[id]; + if (EventPrefab.Prefabs.TryGet(id, out EventPrefab prefab)) + { + yield return prefab; + } } } } } + public readonly float? SelfCommonness; public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f; @@ -157,6 +163,20 @@ namespace Barotrauma commonness = Commonness; probability = Probability; } + + public IEnumerable GetMissingIdentifiers() + { + if (PrefabOrIdentifier.TryCast(out var ids)) + { + foreach (var id in ids) + { + if (!EventPrefab.Prefabs.ContainsKey(id)) + { + yield return id; + } + } + } + } } public readonly ImmutableArray EventPrefabs; @@ -244,6 +264,7 @@ namespace Barotrauma OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); + ResetTime = element.GetAttributeFloat("resettime", 0); DefaultCommonness = 1.0f; foreach (var subElement in element.Elements()) @@ -454,7 +475,6 @@ namespace Barotrauma { childSet.Dispose(); } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index f716173e0..47c73a3eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -39,13 +39,9 @@ namespace Barotrauma targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } - public override bool CanAffectSubImmediately(Level level) - { - return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount; - } - - public override void Init(bool affectSubImmediately) + public override void Init(EventSet parentSet) { + base.Init(parentSet); var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) @@ -60,7 +56,7 @@ namespace Barotrauma if (isFinished) return; if (targetItems.Count == 0 || timer >= duration) { - Finished(); + Finish(); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 78ddfaed8..2ed692fa5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -125,10 +125,6 @@ namespace Barotrauma if (!AllTargetsEliminated()) { return; } State = 1; break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; } } @@ -166,11 +162,16 @@ namespace Barotrauma public override void End() { - if (State == 2) + bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? + campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : + Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); + + if (State > 0 && exitingLevel) { GiveReward(); completed = true; } + failed = !completed && State > 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 8a02928df..58aae000c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -23,15 +22,16 @@ namespace Barotrauma private int calculatedReward; private int maxItemCount; - private Submarine sub; - + private Submarine currentSub; + private SubmarineInfo nextRoundSubInfo; + private readonly List previouslySelectedMissions = new List(); public override LocalizedString Description { get { - if (Submarine.MainSub != sub) + if ((GameMain.GameSession?.Campaign?.PendingSubmarineSwitch ?? Submarine.MainSub?.Info) != nextRoundSubInfo) { string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } @@ -43,7 +43,8 @@ namespace Barotrauma public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - this.sub = sub; + this.currentSub = sub; + this.nextRoundSubInfo = sub?.Info; itemConfig = prefab.ConfigElement.GetChildElement("Items"); requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f); //this can get called between rounds when the client receives a campaign save @@ -57,39 +58,13 @@ namespace Barotrauma private void DetermineCargo() { - if (this.sub == null || itemConfig == null) + if (this.currentSub == null || itemConfig == null) { calculatedReward = Prefab.Reward; return; } itemsToSpawn.Clear(); - List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers(); - containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); - - previouslySelectedMissions.Clear(); - if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) - { - bool isPriorMission = true; - foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) - { - if (!(mission is CargoMission otherMission)) { continue; } - if (mission == this) { isPriorMission = false; } - previouslySelectedMissions.Add(otherMission); - if (!isPriorMission) { continue; } - foreach (var (element, container) in otherMission.itemsToSpawn) - { - for (int i = 0; i < containers.Count; i++) - { - if (containers[i].container == container) - { - containers[i] = (containers[i].container, containers[i].freeSlots - 1); - break; - } - } - } - } - } maxItemCount = 0; foreach (var subElement in itemConfig.Elements()) @@ -98,18 +73,85 @@ namespace Barotrauma maxItemCount += maxCount; } - for (int i = 0; i < containers.Count; i++) + var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (pendingSubInfo != null && pendingSubInfo != currentSub.Info) { - foreach (var subElement in itemConfig.Elements()) + //if we've got a submarine switch pending, calculate the amount of cargo based on it's cargo capacity + //TODO: this isn't guaranteed to be accurate, because we don't take existing items in the new sub's cargo containers + //or items that might get transferred in them into account + maxItemCount = Math.Min(maxItemCount, pendingSubInfo.CargoCapacity); + previouslySelectedMissions.Clear(); + if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { - int maxCount = subElement.GetAttributeInt("maxcount", 10); - if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } - ItemPrefab itemPrefab = FindItemPrefab(subElement); - while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) + bool isPriorMission = true; + foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - containers[i] = (containers[i].container, containers[i].freeSlots - 1); - itemsToSpawn.Add((subElement, containers[i].container)); - if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + if (!(mission is CargoMission otherMission)) { continue; } + if (mission == this) { isPriorMission = false; } + previouslySelectedMissions.Add(otherMission); + if (!isPriorMission) { continue; } + maxItemCount -= otherMission.itemsToSpawn.Count; + } + } + for (int i = 0; i < maxItemCount; i++) + { + foreach (var subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } + ItemPrefab itemPrefab = FindItemPrefab(subElement); + while (itemsToSpawn.Count < maxItemCount) + { + itemsToSpawn.Add((subElement, null)); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + } + } + } + maxItemCount = Math.Max(0, maxItemCount); + nextRoundSubInfo = pendingSubInfo; + } + else + { + List<(ItemContainer container, int freeSlots)> containers = currentSub.GetCargoContainers(); + containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); + + previouslySelectedMissions.Clear(); + if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) + { + bool isPriorMission = true; + foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) + { + if (!(mission is CargoMission otherMission)) { continue; } + if (mission == this) { isPriorMission = false; } + previouslySelectedMissions.Add(otherMission); + if (!isPriorMission) { continue; } + foreach (var (element, container) in otherMission.itemsToSpawn) + { + for (int i = 0; i < containers.Count; i++) + { + if (containers[i].container == container) + { + containers[i] = (containers[i].container, containers[i].freeSlots - 1); + break; + } + } + } + } + } + + for (int i = 0; i < containers.Count; i++) + { + foreach (var subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } + ItemPrefab itemPrefab = FindItemPrefab(subElement); + while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) + { + containers[i] = (containers[i].container, containers[i].freeSlots - 1); + itemsToSpawn.Add((subElement, containers[i].container)); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + } } } } @@ -135,7 +177,7 @@ namespace Barotrauma } if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; } - string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } @@ -167,18 +209,26 @@ namespace Barotrauma } } } - - if (sub != this.sub || missionsChanged) + + var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (pendingSubInfo != null && nextRoundSubInfo != pendingSubInfo) { - this.sub = sub; + this.nextRoundSubInfo = pendingSubInfo; DetermineCargo(); } + else if (sub != this.currentSub || missionsChanged) + { + this.currentSub = sub; + this.nextRoundSubInfo = sub.Info; + DetermineCargo(); + } + return calculatedReward; } private void InitItems() { - this.sub = Submarine.MainSub; + this.currentSub = Submarine.MainSub; DetermineCargo(); items.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index dc062b81e..4975202c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -323,7 +323,7 @@ namespace Barotrauma { var newEvent = eventPrefab.CreateInstance(); GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(true); + newEvent.Init(); } } @@ -382,7 +382,8 @@ namespace Barotrauma #if SERVER totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); #endif - if (totalReward > 0) + bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (isSingleplayerOrServer && totalReward > 0) { campaign.Bank.Give(totalReward); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 796ae18b0..1be2ee4d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -146,8 +146,15 @@ namespace Barotrauma tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); - Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", "")); - Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", "")); + Name = + TextManager.Get($"MissionName.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("name", ""))) + .Fallback(element.GetAttributeString("name", "")); + Description = + TextManager.Get($"MissionDescription.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("description", ""))) + .Fallback(element.GetAttributeString("description", "")); + Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); @@ -160,10 +167,15 @@ namespace Barotrauma Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}").Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); - FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}").Fallback( - TextManager.Get("missionfailed")).Fallback( - GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); + SuccessMessage = + TextManager.Get($"MissionSuccess.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("successmessage", ""))) + .Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); + FailureMessage = + TextManager.Get($"MissionFailure.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("missionfailed", ""))) + .Fallback(TextManager.Get("missionfailed")) + .Fallback(GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); @@ -208,8 +220,14 @@ namespace Barotrauma headers.Add(string.Empty); messages.Add(string.Empty); } - headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", "")); - messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", "")); + headers[messageIndex] = + TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("header", ""))) + .Fallback(subElement.GetAttributeString("header", "")); + messages[messageIndex] = + TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("text", ""))) + .Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index c51ad855c..f861c2d10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -135,8 +135,10 @@ namespace Barotrauma monster.Enabled = false; if (monster.Params.AI != null && monster.Params.AI.EnforceAggressiveBehaviorForMissions) { + monster.Params.AI.FleeHealthThreshold = 0; foreach (var targetParam in monster.Params.AI.Targets) { + if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; } switch (targetParam.State) { case AIState.Avoid: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 6e0b0abc9..20d26fbf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -270,7 +270,7 @@ namespace Barotrauma foreach (Item item in spawnedCharacter.Inventory.AllItems) { - if (item?.Prefab.Identifier == "idcard") + if (item?.GetComponent() != null) { item.AddTag("id_pirate"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 4093d6923..7ca86a794 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -16,6 +16,8 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; private readonly float delayBetweenSpawns; + private float resetTime; + private float resetTimer; private Vector2? spawnPos; @@ -24,7 +26,7 @@ namespace Barotrauma public readonly Level.PositionType SpawnPosType; private readonly string spawnPointTag; - private bool spawnPending; + private bool spawnPending, spawnReady; public readonly int MaxAmountPerLevel = int.MaxValue; @@ -96,6 +98,7 @@ namespace Barotrauma offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); + resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0); if (GameMain.NetworkMember != null) { @@ -131,14 +134,14 @@ namespace Barotrauma } } - public override bool CanAffectSubImmediately(Level level) - { - float maxRange = Sonar.DefaultSonarRange * 0.8f; - return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), GetReferenceSub().WorldPosition) < maxRange * maxRange); - } - - public override void Init(bool affectSubImmediately) + public override void Init(EventSet parentSet) { + base.Init(parentSet); + if (parentSet != null && resetTime == 0) + { + // Use the parent reset time only if there's no reset time defined for the event. + resetTime = parentSet.ResetTime; + } if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); @@ -199,7 +202,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } Submarine refSub = GetReferenceSub(); @@ -267,22 +270,17 @@ namespace Barotrauma if (!isRuinOrWreck) { float minDistance = 20000; - var refSub = GetReferenceSub(); - availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); - if (Submarine.MainSubs.Length > 1) + for (int i = 0; i < Submarine.MainSubs.Length; i++) { - for (int i = 1; i < Submarine.MainSubs.Length; i++) - { - if (Submarine.MainSubs[i] == null) { continue; } - availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); - } + if (Submarine.MainSubs[i] == null) { continue; } + availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); } } if (availablePositions.None()) { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } chosenPosition = availablePositions.GetRandomUnsynced(); @@ -306,7 +304,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } } @@ -344,7 +342,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } } @@ -352,20 +350,42 @@ namespace Barotrauma } } - private float GetMinDistanceToSub(Submarine submarine) + private float GetMinDistanceToSub(Submarine submarine) { - return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); + float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); + if (SpawnPosType.HasFlag(Level.PositionType.Abyss)) + { + minDist *= 2; + } + return minDist; } public override void Update(float deltaTime) { if (disallowed) { - Finished(); + Finish(); return; } - if (isFinished) { return; } + if (resetTimer > 0) + { + resetTimer -= deltaTime; + if (resetTimer <= 0) + { + if (ParentSet?.ResetTime > 0) + { + // If parent has reset time defined, the set is recreated. Otherwise we'll just reset this event. + Finish(); + } + else + { + spawnReady = false; + spawnPos = null; + } + } + return; + } if (spawnPos == null) { @@ -373,7 +393,11 @@ namespace Barotrauma { if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel) { - disallowed = true; + // If the event is set to reset, let's just wait until the old corpse is removed (after being disabled). + if (resetTime == 0) + { + disallowed = true; + } return; } } @@ -384,9 +408,14 @@ namespace Barotrauma spawnPending = true; } - bool spawnReady = false; if (spawnPending) { + System.Diagnostics.Debug.Assert(spawnPos.HasValue); + if (spawnPos == null) + { + Finish(); + return; + } //wait until there are no submarines at the spawnpos if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss)) { @@ -408,7 +437,7 @@ namespace Barotrauma { minDistance = 5000; } - else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) + else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation)) { minDistance = 3000; } @@ -554,28 +583,24 @@ namespace Barotrauma } } - if (!spawnReady) { return; } - - Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter); -#if CLIENT - if (Character.Controlled != null) { targetEntity = Character.Controlled; } -#endif - - bool monstersDead = true; - foreach (Character monster in monsters) + if (spawnReady) { - if (!monster.IsDead) + if (monsters.None()) { - monstersDead = false; - - if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f) + Finish(); + } + else if (monsters.All(m => m.IsDead)) + { + if (resetTime > 0) { - break; + resetTimer = resetTime; + } + else + { + Finish(); } } } - - if (monstersDead) { Finished(); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index c41676ff9..cbb841a31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -12,6 +11,7 @@ namespace Barotrauma private readonly Dictionary> cachedTargets = new Dictionary>(); private int prevEntityCount; private int prevPlayerCount, prevBotCount; + private Character prevControlled; private readonly string[] requiredDestinationTypes; public readonly bool RequireBeaconStation; @@ -163,24 +163,25 @@ namespace Barotrauma botCount++; } } - if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount) + if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) { cachedTargets.Clear(); prevEntityCount = Entity.EntityCount; prevBotCount = botCount; prevPlayerCount = playerCount; + prevControlled = Character.Controlled; } if (!Actions.Any()) { - Finished(); + Finish(); return; } var currentAction = Actions[CurrentActionIndex]; if (!currentAction.CanBeFinished()) { - Finished(); + Finish(); return; } @@ -207,7 +208,7 @@ namespace Barotrauma if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0) { - Finished(); + Finish(); } } else @@ -232,9 +233,9 @@ namespace Barotrauma return false; } - public override void Finished() + public override void Finish() { - base.Finished(); + base.Finish(); GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index d79130b93..ac9e379a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -339,11 +339,6 @@ namespace Barotrauma loadContext = null; assembly = null; } - - ~Implementation() - { - OnQuit(); - } } private static Implementation? loadedImplementation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 7311d6c05..7764c444e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -1,74 +1,141 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { - #warning TODO: This class needs some changes: - // - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods. - // - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism. - // - SpawnItems and SpawnItem are named incorrectly. static class AutoItemPlacer { public static bool OutputDebugInfo = false; - /// - /// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle" - /// - public const float DefaultDifficultyModifier = 0f; - - public static void PlaceIfNeeded() + public static void SpawnItems(Identifier? startItemSet = null) { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } - for (int i = 0; i < Submarine.MainSubs.Length; i++) + //player has more than one sub = we must have given the start items already + bool startItemsGiven = GameMain.GameSession?.OwnedSubmarines != null && GameMain.GameSession.OwnedSubmarines.Count > 1; + if (!startItemsGiven) { - if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; } - List subs = new List() { Submarine.MainSubs[i] }; - subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost)); - Place(subs); - subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); + for (int i = 0; i < Submarine.MainSubs.Length; i++) + { + var sub = Submarine.MainSubs[i]; + if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } + //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) + SpawnStartItems(sub, startItemSet); + //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) + var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); + CreateAndPlace(subs); + subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); + } } - float difficultyModifier = GetLevelDifficultyModifier(); + //spawn items in wrecks, beacon stations and pirate subs foreach (var sub in Submarine.Loaded) { if (sub.Info.Type == SubmarineType.Player || sub.Info.Type == SubmarineType.Outpost || - sub.Info.Type == SubmarineType.OutpostModule || - sub.Info.Type == SubmarineType.EnemySubmarine) + sub.Info.Type == SubmarineType.OutpostModule) { continue; } - Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier); + if (sub.Info.InitialSuppliesSpawned) { continue; } + CreateAndPlace(sub.ToEnumerable()); + sub.Info.InitialSuppliesSpawned = true; } if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost) { - Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name)); - Place(Level.Loaded.StartOutpost.ToEnumerable()); + var sub = Level.Loaded.StartOutpost; + if (!sub.Info.InitialSuppliesSpawned) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(sub.Info.Name)); + CreateAndPlace(sub.ToEnumerable()); + sub.Info.InitialSuppliesSpawned = true; + } } } - private const float MaxDifficultyModifier = 0.2f; - - /// - /// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty. - /// - private static float GetLevelDifficultyModifier() - { - return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier); - } - public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) { - // Level difficulty currently doesn't affect regenerated loot for the sake of simplicity - Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); + CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); } - private static void Place(IEnumerable subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier) + public static Identifier DefaultStartItemSet = new Identifier("normal"); + + /// + /// Spawns the items defined in the start item set in the specified sub. + /// + private static void SpawnStartItems(Submarine sub, Identifier? startItemSet) + { + Identifier setIdentifier = startItemSet ?? DefaultStartItemSet; + if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet)) + { + DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!"); + if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet)) + { + DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!"); + return; + } + itemSet = defaultSet; + } + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); + ISpatialEntity initialSpawnPos; + if (wp?.CurrentHull == null) + { + var spawnHull = Hull.HullList.Where(h => h.Submarine == sub && !h.IsWetRoom).GetRandomUnsynced(); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to spawn start items in the sub. No cargo waypoint or dry hulls found to spawn the items in."); + return; + } + initialSpawnPos = spawnHull; + } + else + { + initialSpawnPos = wp; + } + var newItems = new List(); + foreach (var startItem in itemSet.Items) + { + if (!ItemPrefab.Prefabs.TryGet(startItem.Item, out ItemPrefab itemPrefab)) + { + DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\""); + continue; + } + for (int i = 0; i < startItem.Amount; i++) + { + var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false); + // Is this necessary? + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = sub.TeamID; + } + newItems.Add(item); + } + } + var cargoContainers = new List(); + foreach (var item in newItems) + { +#if SERVER + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); +#endif + foreach (ItemComponent ic in item.Components) + { + ic.OnItemLoaded(); + } + var container = sub.FindContainerFor(item, onlyPrimary: true); + if (container == null) + { + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, initialSpawnPos, ref cargoContainers); + container = cargoContainer?.Item; + } + container?.OwnInventory.TryPutItem(item, user: null); + } + } + + private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -76,7 +143,7 @@ namespace Barotrauma return; } - List spawnedItems = new List(100); + List itemsToSpawn = new List(100); int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); @@ -100,11 +167,11 @@ namespace Barotrauma containers.Shuffle(Rand.RandSync.ServerAndClient); } - foreach (ItemPrefab ip in ItemPrefab.Prefabs) + var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier); + foreach (ItemPrefab ip in itemPrefabs) { - if (!ip.PreferredContainers.Any()) { continue; } - if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && - ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip))) + if (ip.PreferredContainers.None()) { continue; } + if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip))) { prefabsItemsCanSpawnIn.Add(ip); } @@ -141,9 +208,9 @@ namespace Barotrauma { var subNames = subs.Select(s => s.Info.Name).ToList(); DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:"); - foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct()) + foreach (string itemName in itemsToSpawn.Select(it => it.Name).Distinct()) { - DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName)); + DebugConsole.NewMessage(" - " + itemName + " x" + itemsToSpawn.Count(it => it.Name == itemName)); } } @@ -153,24 +220,28 @@ namespace Barotrauma { foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems) { - var matchingItem = spawnedItems.Find(it => takenItem.Matches(it)); + var matchingItem = itemsToSpawn.Find(it => takenItem.Matches(it)); if (matchingItem == null) { continue; } - var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem); + if (OutputDebugInfo) + { + DebugConsole.NewMessage($"Removing the stolen item: {matchingItem.Prefab.Identifier} ({matchingItem.ID})"); + } + var containedItems = itemsToSpawn.FindAll(it => it.ParentInventory?.Owner == matchingItem); matchingItem.Remove(); - spawnedItems.Remove(matchingItem); + itemsToSpawn.Remove(matchingItem); foreach (Item containedItem in containedItems) { containedItem.Remove(); - spawnedItems.Remove(containedItem); + itemsToSpawn.Remove(containedItem); } } } - foreach (Item spawnedItem in spawnedItems) + foreach (Item item in itemsToSpawn) { #if SERVER - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem)); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - foreach (ItemComponent ic in spawnedItem.Components) + foreach (ItemComponent ic in item.Components) { ic.OnItemLoaded(); } @@ -186,9 +257,12 @@ namespace Barotrauma return false; } bool success = false; + bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) { - if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; } + if (preferredContainer.CampaignOnly && !isCampaign) { continue; } + if (preferredContainer.NotCampaign && isCampaign) { continue; } + if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; } validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true); if (validContainers.None()) { @@ -196,10 +270,10 @@ namespace Barotrauma } foreach (var validContainer in validContainers) { - var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier); + var newItems = CreateItems(itemPrefab, containers, validContainer); if (newItems.Any()) { - spawnedItems.AddRange(newItems); + itemsToSpawn.AddRange(newItems); success = true; } } @@ -238,16 +312,20 @@ namespace Barotrauma (3, 0.0f), }; - private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) + private static List CreateItems(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { - List spawnedItems = new List(); - if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } + List newItems = new List(); + if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability) { return newItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { - return spawnedItems; + return newItems; + } + int amount = validContainer.Value.Amount; + if (amount == 0) + { + amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient); } - int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient); for (int i = 0; i < amount; i++) { if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true)) @@ -255,14 +333,12 @@ namespace Barotrauma containers.Remove(validContainer.Key); break; } - var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab); int quality = existingItem?.Quality ?? ToolBox.SelectWeightedRandom( qualityCommonnesses.Select(q => q.quality).ToList(), - qualityCommonnesses.Select(q => q.commonness).ToList(), - Rand.RandSync.ServerAndClient); + qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient); if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { @@ -277,11 +353,11 @@ namespace Barotrauma { wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID; } - spawnedItems.Add(item); + newItems.Add(item); validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false); containers.AddRange(item.GetComponents()); } - return spawnedItems; + return newItems; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 2492beea5..b14a19dde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -22,14 +22,14 @@ namespace Barotrauma public int Quantity { get; set; } public bool? IsStoreComponentEnabled { get; set; } - public readonly int BuyerCharacterInfoId; + public readonly int BuyerCharacterInfoIdentifier; public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefabIdentifier = itemPrefab.Identifier; Quantity = quantity; IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyerCharacterInfoId; + BuyerCharacterInfoIdentifier = buyerCharacterInfoId; } #if CLIENT @@ -44,7 +44,7 @@ namespace Barotrauma ItemPrefabIdentifier = itemPrefabId; Quantity = quantity; IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; + BuyerCharacterInfoIdentifier = buyer?.Character?.Info?.GetIdentifier() ?? Character.Controlled?.Info?.GetIdentifier() ?? 0; } public override string ToString() @@ -284,11 +284,10 @@ namespace Barotrauma foreach (PurchasedItem item in newItems) { int itemValue = item.Quantity * buyValues[item.ItemPrefab]; + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}"); price += itemValue; - } - GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money); } #endif @@ -317,7 +316,10 @@ namespace Barotrauma // Exchange money int itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.TryPurchase(client, itemValue); - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + if (GameMain.IsSingleplayer) + { + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + } store.Balance += itemValue; if (removeFromCrate) { @@ -368,12 +370,13 @@ namespace Barotrauma public void CreatePurchasedItems() { + purchasedIDCards.Clear(); var items = new List(); foreach (var storeSpecificItems in PurchasedItems) { items.AddRange(storeSpecificItems.Value); } - CreateItems(items, Submarine.MainSub); + CreateItems(items, Submarine.MainSub, this); PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(); } @@ -407,7 +410,7 @@ namespace Barotrauma if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; } + if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -428,7 +431,7 @@ namespace Barotrauma if (!item.Prefab.CanBeSold) { return false; } if (item.SpawnedInCurrentOutpost) { return false; } if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedItems.Any(ci => ci.Item == item)) { return false; } + if (confirmedItems != null && confirmedItems.Any(ci => ci.Item == item)) { return false; } if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count)) { int newCount = count - 1; @@ -448,13 +451,58 @@ namespace Barotrauma if (containedItems.None()) { return true; } // Allow selling the item if contained items are unsellable and set to be removed on deconstruct if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } - // Otherwise there must be no contained items or the contained items must be confirmed as sold - if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + if (confirmedItems != null) + { + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + } } return true; } - public static void CreateItems(List itemsToSpawn, Submarine sub) + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) + { + ItemContainer itemContainer = null; + if (!string.IsNullOrEmpty(item.CargoContainerIdentifier)) + { + itemContainer = availableContainers.Find(ac => + ac.Inventory.CanBePut(item) && + (ac.Item.Prefab.Identifier == item.CargoContainerIdentifier || + ac.Item.Prefab.Tags.Contains(item.CargoContainerIdentifier))); + + if (itemContainer == null) + { + ItemPrefab containerPrefab = ItemPrefab.Prefabs.Find(ep => + ep.Identifier == item.CargoContainerIdentifier || + (ep.Tags != null && ep.Tags.Contains(item.CargoContainerIdentifier))); + + if (containerPrefab == null) + { + DebugConsole.AddWarning($"CargoManager: could not find the item prefab for container {item.CargoContainerIdentifier}!"); + return null; + } + + Vector2 containerPosition = cargoRoomOrSpawnPoint is Hull cargoRoom ? GetCargoPos(cargoRoom, containerPrefab) : cargoRoomOrSpawnPoint.Position; + Item containerItem = new Item(containerPrefab, containerPosition, cargoRoomOrSpawnPoint.Submarine); + itemContainer = containerItem.GetComponent(); + if (itemContainer == null) + { + DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!"); + return null; + } + availableContainers.Add(itemContainer); +#if SERVER + if (GameMain.Server != null) + { + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); + } +#endif + } + } + return itemContainer; + } + + public static void CreateItems(List itemsToSpawn, Submarine sub, CargoManager cargoManager) { if (itemsToSpawn.Count == 0) { return; } @@ -496,60 +544,26 @@ namespace Barotrauma } List availableContainers = new List(); - ItemPrefab containerPrefab = null; foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); for (int i = 0; i < pi.Quantity; i++) { - ItemContainer itemContainer = null; - if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) - { - itemContainer = availableContainers.Find(ac => - ac.Inventory.CanBePut(pi.ItemPrefab) && - (ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); - - if (itemContainer == null) - { - containerPrefab = ItemPrefab.Prefabs.Find(ep => - ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - (ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); - - if (containerPrefab == null) - { - DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!"); - continue; - } - - Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab); - Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine); - itemContainer = containerItem.GetComponent(); - if (itemContainer == null) - { - DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!"); - continue; - } - availableContainers.Add(itemContainer); -#if SERVER - if (GameMain.Server != null) - { - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); - } -#endif - } - } - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemContainer?.Inventory.TryPutItem(item, null); - - itemSpawned(item); + var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers); + itemContainer?.Inventory.TryPutItem(item, null); + var idCard = item.GetComponent(); + if (cargoManager != null && idCard != null && pi.BuyerCharacterInfoIdentifier != 0) + { + cargoManager.purchasedIDCards.Add((pi, idCard)); + } + itemSpawned(pi, item); #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; - static void itemSpawned(Item item) + static void itemSpawned(PurchasedItem purchased, Item item) { Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; if (sub != null) @@ -565,6 +579,23 @@ namespace Barotrauma itemsToSpawn.Clear(); } + private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>(); + public void InitPurchasedIDCards() + { + foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards) + { + if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) + { + var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier); + if (owner?.Info != null) + { + var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List() { owner.Info }, Submarine.MainSub); + idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner); + } + } + } + } + public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab) { float floorPos = hull.Rect.Y - hull.Rect.Height; @@ -603,7 +634,7 @@ namespace Barotrauma new XAttribute("id", item.ItemPrefab.Identifier), new XAttribute("qty", item.Quantity), new XAttribute("storeid", storeSpecificItems.Key), - new XAttribute("buyer", item.BuyerCharacterInfoId))); + new XAttribute("buyer", item.BuyerCharacterInfoIdentifier))); } } parentElement.Add(itemsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 6bf837b8b..421af50e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -51,8 +51,6 @@ namespace Barotrauma public ReadyCheck ActiveReadyCheck; - public XElement ActiveOrdersElement { get; set; } - public CrewManager(bool isSinglePlayer) { IsSinglePlayer = isSinglePlayer; @@ -493,9 +491,8 @@ namespace Barotrauma partial void UpdateProjectSpecific(float deltaTime); - private void SaveActiveOrders(XElement parentElement) + public void SaveActiveOrders(XElement element) { - ActiveOrdersElement = new XElement("activeorders"); // Only save orders with no fade out time (e.g. ignore orders) var ordersToSave = new List(); foreach (var activeOrder in ActiveOrders) @@ -504,14 +501,13 @@ namespace Barotrauma if (order == null || activeOrder.FadeOutTime.HasValue) { continue; } ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority)); } - CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray()); - parentElement?.Add(ActiveOrdersElement); + CharacterInfo.SaveOrders(element, ordersToSave.ToArray()); } - public void LoadActiveOrders() + public void LoadActiveOrders(XElement element) { - if (ActiveOrdersElement == null) { return; } - foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement)) + if (element == null) { return; } + foreach (var orderInfo in CharacterInfo.LoadOrders(element)) { IIgnorable ignoreTarget = null; if (orderInfo.IsIgnoreOrder) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 019a75046..9edca9159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -9,63 +10,6 @@ using System.Xml.Linq; namespace Barotrauma { - internal struct CampaignSettings - { - public static CampaignSettings Empty => new CampaignSettings(); - - // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down - public static CampaignSettings Unsure => Empty; - public bool RadiationEnabled { get; set; } - - public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); - - private int maxMissionCount; - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); } - } - - public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; - public const int MinMissionCountLimit = 1; - - public CampaignSettings(IReadMessage inc) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = inc.ReadBoolean(); - MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit); - } - - public CampaignSettings(XElement element) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); - MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount); - } - - public void Serialize(IWriteMessage msg) - { - msg.Write(RadiationEnabled); - msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit); - } - - public int GetAddedMissionCount() - { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; - } - - public XElement Save() - { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount)); - } - } - abstract partial class CampaignMode : GameMode { [NetworkSerialize] @@ -107,6 +51,8 @@ namespace Barotrauma protected XElement petsElement; + protected XElement ActiveOrdersElement { get; set; } + public CampaignSettings Settings; private readonly List extraMissions = new List(); @@ -146,9 +92,8 @@ namespace Barotrauma //key = dialog flag, double = Timing.TotalTime when the line was last said private readonly Dictionary dialogLastSpoken = new Dictionary(); - public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs; - public SubmarineInfo PendingSubmarineSwitch; + public bool TransferItemsOnSubSwitch { get; set; } protected Map map; public Map Map @@ -186,12 +131,16 @@ namespace Barotrauma protected set; } - protected CampaignMode(GameModePreset preset) + public virtual bool PurchasedHullRepairs { get; set; } + public virtual bool PurchasedLostShuttles { get; set; } + public virtual bool PurchasedItemRepairs { get; set; } + + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { Bank = new Wallet(Option.None()) { - Balance = InitialMoney + Balance = settings.InitialMoney }; CargoManager = new CargoManager(this); @@ -558,6 +507,8 @@ namespace Barotrauma } } + public TransitionType GetAvailableTransition() => GetAvailableTransition(out _, out _); + /// /// Which submarine is at a position where it can leave the level and enter another one (if any). /// @@ -593,6 +544,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost == null) { Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -726,7 +678,6 @@ namespace Barotrauma } } - public void EndCampaign() { foreach (Character c in Character.CharacterList) @@ -738,13 +689,16 @@ namespace Barotrauma } foreach (LocationConnection connection in Map.Connections) { - connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f); - connection.LevelData.Difficulty = connection.Difficulty; - connection.LevelData.IsBeaconActive = false; + connection.Difficulty = connection.Biome.MaxDifficulty; + connection.LevelData = new LevelData(connection) + { + IsBeaconActive = false + }; connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds; } foreach (Location location in Map.Locations) { + location.LevelData = new LevelData(location, location.Biome.MaxDifficulty); location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -868,7 +822,7 @@ namespace Barotrauma const float MaxDist = 3000.0f; const float MinDist = 2500.0f; - if (!Level.IsLoadedOutpost) { return; } + if (!Level.IsLoadedFriendlyOutpost) { return; } Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); @@ -1032,5 +986,190 @@ namespace Barotrauma } } + protected void LeaveUnconnectedSubs(Submarine leavingSub) + { + if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) + { + Submarine.MainSub = leavingSub; + GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); + var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); + foreach (Submarine sub in subsToLeaveBehind) + { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); + MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); + LinkedSubmarine.CreateDummy(leavingSub, sub); + } + } + } + + public SubmarineInfo SwitchSubs() + { + if (TransferItemsOnSubSwitch) + { + TransferItemsBetweenSubs(); + } + RefreshOwnedSubmarines(); + PendingSubmarineSwitch = null; + return GameMain.GameSession.SubmarineInfo; + } + + /// + /// Also serializes the current sub. + /// + protected void TransferItemsBetweenSubs() + { + Submarine currentSub = GameMain.GameSession.Submarine; + if (currentSub == null || currentSub.Removed) + { + DebugConsole.ThrowError("Cannot transfer items between subs, because the current sub is null or removed!"); + return; + } + var itemsToTransfer = new List<(Item item, Item container)>(); + if (PendingSubmarineSwitch != null) + { + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + // Remove items from the old sub + foreach (Item item in Item.ItemList) + { + if (item.Removed) { continue; } + if (item.NonInteractable) { continue; } + if (item.HiddenInGame) { continue; } + if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (item.Prefab.DontTransferBetweenSubs) { continue; } + if (item.GetRootInventoryOwner() is Character) { continue; } + if (item.GetComponent() == null && item.GetComponent() == null && item.GetComponent() == null) { continue; } + if (item.Components.Any(c => c is Holdable h && h.Attached)) { continue; } + if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } + itemsToTransfer.Add((item, item.Container)); + item.Submarine = null; + } + foreach (var (item, container) in itemsToTransfer) + { + if (container?.Submarine != null) + { + // Drop the item if it's not inside another item set to be transferred. + item.Drop(null, createNetworkEvent: false, setTransform: false); + } + } + } + // Serialize the current sub + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(currentSub); + if (PendingSubmarineSwitch != null && itemsToTransfer.Any()) + { + // Load the new sub + var newSub = new Submarine(PendingSubmarineSwitch); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + // Move the transferred items + List availableContainers = Item.ItemList + .Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed) + .Select(it => it.GetComponent()) + .Where(c => c != null) + .ToList(); + foreach (var (item, oldContainer) in itemsToTransfer) + { + Item newContainer = null; + item.Submarine = newSub; + if (item.Container == null) + { + newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true); + } + if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) + { + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced(); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + if (spawnHull != null) + { + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) + { + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); + } + } + else + { + DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + } + } + string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; + string msg = "Item transfer log error."; + if (oldContainer != null) + { + if (newContainer == null && oldContainer == item.Container) + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})"; + } + else + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}"; + } + } + else + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}"; + } +#if DEBUG + DebugConsole.NewMessage(msg); +#else + DebugConsole.Log(msg); +#endif + } + // Serialize the new sub + PendingSubmarineSwitch = new SubmarineInfo(newSub); + } + } + + protected void RefreshOwnedSubmarines() + { + if (PendingSubmarineSwitch != null) + { + SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; + GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; + + for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) + { + GameMain.GameSession.OwnedSubmarines[i] = previousSub; + break; + } + } + } + } + + public void SavePets(XElement parentElement = null) + { + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + parentElement?.Add(petsElement); + } + + public void LoadPets() + { + if (petsElement != null) + { + PetBehavior.LoadPets(petsElement); + } + } + + public void SaveActiveOrders(XElement parentElement = null) + { + ActiveOrdersElement = new XElement("activeorders"); + CrewManager?.SaveActiveOrders(ActiveOrdersElement); + parentElement?.Add(ActiveOrdersElement); + } + + public void LoadActiveOrders() + { + CrewManager?.LoadActiveOrders(ActiveOrdersElement); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs new file mode 100644 index 000000000..fd5297d89 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal static class CampaignModePresets + { + public static readonly ImmutableArray List; + public static readonly ImmutableDictionary Definitions; + + private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml"); + + static CampaignModePresets() + { + if (!File.Exists(fileListPath) || !(XMLExtensions.TryLoadXml(fileListPath)?.Root is { } docRoot)) + { + List = ImmutableArray.Empty; + return; + } + + List list = new List(); + Dictionary definitions = new Dictionary(); + + foreach (XElement element in docRoot.Elements()) + { + Identifier name = element.NameAsIdentifier(); + + if (name == CampaignSettings.LowerCaseSaveElementName) + { + list.Add(new CampaignSettings(element)); + } + else if (name == nameof(CampaignSettingDefinitions)) + { + foreach (XElement subElement in element.Elements()) + { + definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); + } + } + } + + List = list.ToImmutableArray(); + Definitions = definitions.ToImmutableDictionary(); + } + } + + internal readonly struct CampaignSettingDefinitions + { + // Definitely not the best way to do this + private readonly ImmutableDictionary> values; + + public CampaignSettingDefinitions(XElement element) + { + var definitions = new Dictionary>(); + foreach (XAttribute attribute in element.Attributes()) + { + Identifier name = attribute.NameAsIdentifier(); + if (attribute.Value.Contains('.')) + { + definitions.Add(name, element.GetAttributeFloat(name.Value, 0)); + } + else + { + definitions.Add(name, element.GetAttributeInt(name.Value, 0)); + } + } + + values = definitions.ToImmutableDictionary(); + } + + public float GetFloat(Identifier identifier) + { + float range = 0; + if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out range)) + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); + } + return range; + } + + public int GetInt(Identifier identifier) + { + int integer = 0; + if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out integer)) + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); + } + return integer; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs new file mode 100644 index 000000000..1d96fa2fa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -0,0 +1,114 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class CampaignSettings : INetSerializableStruct, ISerializableEntity + { + public static CampaignSettings Empty => new CampaignSettings(element: null); + + public string Name => "CampaignSettings"; + + public const string LowerCaseSaveElementName = "campaignsettings"; + + [Serialize("", IsPropertySaveable.Yes)] + public string PresetName { get; set; } = string.Empty; + + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] + public bool RadiationEnabled { get; set; } + + private int maxMissionCount; + + [Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)] + public int MaxMissionCount + { + get => maxMissionCount; + set => maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); + } + + public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); + + [Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public StartingBalanceAmount StartingBalanceAmount { get; set; } + + [Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public GameDifficulty Difficulty { get; set; } + + [Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize] + public Identifier StartItemSet { get; set; } + + public int InitialMoney + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + return definition.GetInt(StartingBalanceAmount.ToIdentifier()); + } + return 8000; + + } + } + + public float ExtraEventManagerDifficulty + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 0; + } + } + + public float LevelDifficultyMultiplier + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 1.0f; + } + } + + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; + + public Dictionary SerializableProperties { get; private set; } + + // required for INetSerializableStruct + public CampaignSettings() + { + SerializableProperties = SerializableProperty.GetProperties(this); + } + + public CampaignSettings(XElement? element = null) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public XElement Save() + { + XElement saveElement = new XElement(LowerCaseSaveElementName); + SerializableProperty.SerializeProperties(this, saveElement, saveIfDefault: true); + return saveElement; + } + + private static int GetAddedMissionCount() + { + int count = 0; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); + } + return count; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index d8871d2bc..789a31c8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -29,7 +29,11 @@ namespace Barotrauma : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - missions.Add(Mission.LoadRandom(locations, seed, false, missionType)); + var mission = Mission.LoadRandom(locations, seed, false, missionType); + if (mission != null) + { + missions.Add(mission); + } } protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a8c7034ef..18a0f6fcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,19 +12,60 @@ namespace Barotrauma { public const int MinimumInitialMoney = 500; - private UInt16 lastUpdateID; - public UInt16 LastUpdateID + [Flags] + public enum NetFlags : UInt16 { - get - { -#if SERVER - if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; } -#endif - return lastUpdateID; - } - set { lastUpdateID = value; } + Misc = 0x1, + MapAndMissions = 0x2, + UpgradeManager = 0x4, + SubList = 0x8, + ItemsInBuyCrate = 0x10, + ItemsInSellFromSubCrate = 0x20, + PurchasedItems = 0x80, + SoldItems = 0x100, + Reputation = 0x200, + CharacterInfo = 0x800 } + private readonly Dictionary lastUpdateID; + + public UInt16 GetLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return 0; } + return lastUpdateID[flag]; + } + public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id) + { + if (!ValidateFlag(flag)) { return; } + lastUpdateID[flag] = id; + } + + public void IncrementLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return; } + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + public void IncrementAllLastUpdateIds() + { + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + } + + private bool ValidateFlag(NetFlags flag) + { + if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } +#if DEBUG + throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag."); +#else + return false; +#endif + } + + private UInt16 lastSaveID; public UInt16 LastSaveID { @@ -35,11 +76,11 @@ namespace Barotrauma #endif return lastSaveID; } - set + set { #if SERVER //trigger a campaign update to notify the clients of the changed save ID - lastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.Misc); #endif lastSaveID = value; } @@ -52,23 +93,33 @@ namespace Barotrauma get; set; } - private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign) + private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings) { currentCampaignID++; + lastUpdateID = new Dictionary(); + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { +#if SERVER + //server starts from a higher ID to ensure we send the initial state + lastUpdateID[flag] = 1; +#else + lastUpdateID[flag] = 0; +#endif + } CampaignID = currentCampaignID; CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); InitCampaignData(); } - public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) + public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings); //only the server generates the map, the clients load it from a save file if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - campaign.map = new Map(campaign, mapSeed, settings); campaign.Settings = settings; + campaign.map = new Map(campaign, mapSeed); } campaign.InitProjSpecific(); return campaign; @@ -76,7 +127,7 @@ namespace Barotrauma public static MultiPlayerCampaign LoadNew(XElement element) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty); campaign.Load(element); campaign.InitProjSpecific(); campaign.IsFirstRound = false; @@ -124,18 +175,17 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); #if CLIENT - GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount; - GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled; + GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings; #endif break; case "map": if (map == null) { //map not created yet, loading this campaign for the first time - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); } else { @@ -155,7 +205,7 @@ namespace Barotrauma case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer: CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false); CrewManager.AddCharacterElements(subElement); - CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders"); + ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "cargo": CargoManager?.LoadPurchasedItems(subElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index a7b79214f..68bfe6969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -72,7 +72,7 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -83,7 +83,7 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -207,7 +207,7 @@ namespace Barotrauma } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { - var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.Deduct(selectedSub.Price); @@ -218,7 +218,7 @@ namespace Barotrauma #if CLIENT else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { - var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.TryDeduct(selectedSub.Price); @@ -245,25 +245,15 @@ namespace Barotrauma } } - private void CreateDummyLocations(LocationType? forceLocationType = null) + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) { - dummyLocations = new Location[2]; - - string seed = ""; - if (GameMain.GameSession != null && GameMain.GameSession.Level != null) - { - seed = GameMain.GameSession.Level.Seed; - } - else if (GameMain.NetLobbyScreen != null) - { - seed = GameMain.NetLobbyScreen.LevelSeed; - } - + var dummyLocations = new Location[2]; MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); } + return dummyLocations; } public void LoadPreviousSave() @@ -275,7 +265,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -293,15 +283,13 @@ namespace Barotrauma } } } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) { Campaign!.TryPurchase(client, cost); } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; - - return newSubmarine; + Campaign!.TransferItemsOnSubSwitch = transferItems; } public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) @@ -312,6 +300,9 @@ namespace Barotrauma { GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); +#if SERVER + (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); +#endif } } @@ -348,7 +339,7 @@ namespace Barotrauma !missionPrefab.AllowedConnectionTypes.Any()) { LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); - CreateDummyLocations(locationType); + dummyLocations = CreateDummyLocations(levelSeed, locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } @@ -433,7 +424,7 @@ namespace Barotrauma Level? level = null; if (levelData != null) { - level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost); + level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost); } InitializeLevel(level); @@ -600,10 +591,13 @@ namespace Barotrauma { //only place items and corpses here in single player //the server does this after loading the respawn shuttle - Level?.SpawnNPCs(); - Level?.SpawnCorpses(); - Level?.PrepareBeaconStation(); - AutoItemPlacer.PlaceIfNeeded(); + if (Level != null) + { + Level.SpawnNPCs(); + Level.SpawnCorpses(); + Level.PrepareBeaconStation(); + } + AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet); } if (GameMode is MultiPlayerCampaign mpCampaign) { @@ -628,8 +622,6 @@ namespace Barotrauma return; } - var originalSubPos = Submarine.WorldPosition; - if (level.StartOutpost != null) { //start by placing the sub below the outpost @@ -712,7 +704,7 @@ namespace Barotrauma if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; } if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; } if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; } - ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos)); + ls.SetPositionRelativeToMainSub(); } } @@ -836,6 +828,11 @@ namespace Barotrauma { GUI.TogglePauseMenu(); } + if (IsTabMenuOpen) + { + ToggleTabMenu(); + } + GUI.PreventPauseMenuToggle = true; if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) @@ -1072,8 +1069,16 @@ namespace Barotrauma rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); rootElement.Add(new XAttribute("version", GameMain.Version)); - var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo; - rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); + if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) + { + bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null && + Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation; + if (hasNewPendingSub) + { + Campaign.SwitchSubs(); + } + } + rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name)); if (OwnedSubmarines != null) { List ownedSubmarineNames = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs index a1b6bed04..689569ff4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs @@ -13,12 +13,26 @@ namespace Barotrauma internal partial class ReadyCheck { - private readonly float endTime; - private float time; + private readonly DateTime endTime; + private readonly DateTime startTime; public readonly Dictionary Clients; public bool IsFinished = false; - public ReadyCheck(List clients, float duration = 30) + public ReadyCheck(List clients, DateTime startTime, DateTime endTime) + : this(clients) + { + this.startTime = startTime; + this.endTime = endTime; + } + + public ReadyCheck(List clients, float duration) + : this(clients) + { + startTime = DateTime.Now; + endTime = startTime + new TimeSpan(0, 0, 0, 0, (int)(duration * 1000)); + } + + private ReadyCheck(List clients) { Clients = new Dictionary(); foreach (byte client in clients) @@ -27,24 +41,17 @@ namespace Barotrauma Clients.Add(client, ReadyStatus.Unanswered); } - - time = duration; - endTime = duration; -#if CLIENT - lastSecond = (int) Math.Ceiling(duration); -#endif } partial void EndReadyCheck(); public void Update(float deltaTime) { - if (time > 0) + if (DateTime.Now < endTime) { #if CLIENT UpdateBar(); #endif - time -= deltaTime; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index fcac2eba3..3e792f98d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -13,14 +13,14 @@ namespace Barotrauma SelectNextCharacter, SelectPreviousCharacter, Voice, - LocalVoice, Deselect, Shoot, Command, - ToggleInventory, TakeOneFromInventorySlot, TakeHalfFromInventorySlot, NextFireMode, - PreviousFireMode + PreviousFireMode, + ActiveChat, + ToggleChatMode, } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index cb24eb337..a660bbbf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -360,9 +360,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item)) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List { InvSlotType.Any }, true, ignoreCondition)) { free = false; @@ -382,9 +379,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty()) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif bool removeFromOtherSlots = item.ParentInventory != this; if (placedInSlot == -1 && inWrongSlot) { @@ -454,9 +448,6 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; } -#endif //there's already an item in the slot if (slots[index].Any()) { @@ -480,9 +471,6 @@ namespace Barotrauma foreach (InvSlotType allowedSlot in pickable.AllowedSlots) { if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } - #if CLIENT - if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } - #endif for (int i = 0; i < capacity; i++) { if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index f8249369e..405b6a22a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -99,8 +99,8 @@ namespace Barotrauma.Items.Components { if (!docked && value) { - if (DockingTarget == null) AttemptDock(); - if (DockingTarget == null) return; + if (DockingTarget == null) { AttemptDock(); } + if (DockingTarget == null) { return; } docked = true; } @@ -126,6 +126,14 @@ namespace Barotrauma.Items.Components /// public event Action OnUnDocked; + private bool outpostAutoDockingPromptShown; + + enum AllowOutpostAutoDocking + { + Ask, Yes, No + } + private AllowOutpostAutoDocking allowOutpostAutoDocking = AllowOutpostAutoDocking.Ask; + public DockingPort(Item item, ContentXElement element) : base(item, element) { @@ -613,7 +621,7 @@ namespace Barotrauma.Items.Components hullRects[i].X -= expand; hullRects[i].Width += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i] = new Hull(hullRects[i], subs[i]); hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -622,7 +630,8 @@ namespace Barotrauma.Items.Components { bodies[i + j * 2] = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X, hullRects[i].Y - hullRects[i].Height * j)), - ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j))); + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)), + BodyType.Static); } } @@ -632,7 +641,9 @@ namespace Barotrauma.Items.Components ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width), ConvertUnits.ToSimUnits(hullRects[0].Height), density: 0.0f, - offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition)); + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition), + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile); outsideBlocker.UserData = this; } @@ -733,7 +744,7 @@ namespace Barotrauma.Items.Components hullRects[i].Y += expand; hullRects[i].Height += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i] = new Hull(hullRects[i], subs[i]); hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -742,7 +753,8 @@ namespace Barotrauma.Items.Components { bodies[i + j * 2] = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)), - ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height))); + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)), + BodyType.Static); } } @@ -752,7 +764,9 @@ namespace Barotrauma.Items.Components ConvertUnits.ToSimUnits(hullRects[0].Width), ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height), density: 0.0f, - offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition)); + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition), + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile); outsideBlocker.UserData = this; } @@ -778,8 +792,6 @@ namespace Barotrauma.Items.Components if (body == null) { continue; } body.BodyType = BodyType.Static; body.Friction = 0.5f; - - body.CollisionCategories = Physics.CollisionWall; } } @@ -947,7 +959,7 @@ namespace Barotrauma.Items.Components { foreach (Body body in bodies) { - if (body == null) continue; + if (body == null) { continue; } GameMain.World.Remove(body); } bodies = null; @@ -961,6 +973,9 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } +#elif CLIENT + autodockingVerification?.Close(); + autodockingVerification = null; #endif OnUnDocked?.Invoke(); OnUnDocked = null; @@ -1140,27 +1155,86 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } +#if CLIENT + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && + !(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(ClientPermissions.ManageMap) ?? false)) + { + return; + } +#endif if (dockingCooldown > 0.0f) { return; } bool wasDocked = docked; DockingPort prevDockingTarget = DockingTarget; + bool newDockedState = wasDocked; switch (connection.Name) { case "toggle": if (signal.value != "0") { - Docked = !docked; + newDockedState = !docked; } break; case "set_active": case "set_state": - Docked = signal.value != "0"; + newDockedState = signal.value != "0"; break; } + if (newDockedState != wasDocked) + { + bool tryingToToggleOutpostDocking = docked ? + DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false : + FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false; + //trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character + // -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port + // in a way that makes always makes it dock/undock immediately at the start of the roun + if (tryingToToggleOutpostDocking && signal.sender == null) + { + if (allowOutpostAutoDocking == AllowOutpostAutoDocking.Ask) + { +#if CLIENT + if (!outpostAutoDockingPromptShown) + { + autodockingVerification = new GUIMessageBox(string.Empty, + TextManager.Get(newDockedState ? "autodockverification" : "autoundockverification"), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + autodockingVerification.Buttons[0].OnClicked += (btn, userdata) => + { + autodockingVerification?.Close(); + autodockingVerification = null; + if (item.Removed || GameMain.Client == null) { return false; } + allowOutpostAutoDocking = AllowOutpostAutoDocking.Yes; + item.CreateClientEvent(this); + return true; + }; + autodockingVerification.Buttons[1].OnClicked += (btn, userdata) => + { + autodockingVerification?.Close(); + autodockingVerification = null; + if (item.Removed || GameMain.Client == null) { return false; } + allowOutpostAutoDocking = AllowOutpostAutoDocking.No; + item.CreateClientEvent(this); + return true; + }; + } +#endif + outpostAutoDockingPromptShown = true; + return; + } + else if (allowOutpostAutoDocking == AllowOutpostAutoDocking.No) + { + return; + } + } + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + Docked = newDockedState; + } + #if SERVER if (signal.sender != null && docked != wasDocked) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 98dc5114d..aa26c2e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -241,12 +241,14 @@ namespace Barotrauma.Items.Components Body = new PhysicsBody( ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)), ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)), - 0.0f, - 1.5f) + radius: 0.0f, + density: 1.5f, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile, + findNewContacts: false) { UserData = item, - CollisionCategories = Physics.CollisionWall, - BodyType = BodyType.Static, Friction = 0.5f }; Body.SetTransformIgnoreContacts( @@ -258,12 +260,16 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { - base.Move(amount); - - Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); - + if (ignoreContacts) + { + Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } + else + { + Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } #if CLIENT UpdateConvexHulls(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index d8543526e..059832004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -144,9 +144,16 @@ namespace Barotrauma.Items.Components } } - public bool Combine(GeneticMaterial otherGeneticMaterial, Character user) + public enum CombineResult { - if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } + None, + Refined, + Combined + } + + public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user) + { + if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; } float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; @@ -158,7 +165,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return true; + return CombineResult.Refined; } else { @@ -171,7 +178,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return false; + return CombineResult.Combined; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 384b88db6..0fbda966d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components private Character prevEquipper; + public override bool IsAttached => Attached; + private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; private PhysicsBody body; @@ -71,6 +73,7 @@ namespace Barotrauma.Items.Components set { attached = value; + item.CheckCleanable(); item.SetActiveSprite(); } } @@ -164,6 +167,8 @@ namespace Barotrauma.Items.Components public bool SwingWhenAiming { get; set; } [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] public bool SwingWhenUsing { get; set; } + [Editable, Serialize(false, IsPropertySaveable.No)] + public bool DisableHeadRotation { get; set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] public float SpriteDepthWhenDropped @@ -180,11 +185,12 @@ namespace Barotrauma.Items.Components Pusher = null; if (element.GetAttributeBool("blocksplayers", false)) { - Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density) + Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, + item.body.Density, + BodyType.Dynamic, + Physics.CollisionItemBlocking, + Physics.CollisionCharacter | Physics.CollisionProjectile) { - BodyType = BodyType.Dynamic, - CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile, - CollisionCategories = Physics.CollisionItemBlocking, Enabled = false, UserData = this }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 0dee7a4a2..4ad251c17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -3,7 +3,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -79,11 +78,18 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (trigger != null && amount.LengthSquared() > 0.00001f) { - trigger.SetTransform(item.SimPosition, 0.0f); + if (ignoreContacts) + { + trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + } + else + { + trigger.SetTransform(item.SimPosition, 0.0f); + } } } @@ -119,17 +125,19 @@ namespace Barotrauma.Items.Components } var body = item.body ?? holdable.Body; - + if (body != null) { - trigger = new PhysicsBody(body.width, body.height, body.radius, body.Density) + trigger = new PhysicsBody(body.width, body.height, body.radius, + body.Density, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionNone, + findNewContacts: false) { UserData = item }; trigger.FarseerBody.SetIsSensor(true); - trigger.FarseerBody.BodyType = BodyType.Static; - trigger.FarseerBody.CollisionCategories = Physics.CollisionWall; - trigger.FarseerBody.CollidesWith = Physics.CollisionNone; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 353a0b0f6..53a55e033 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -111,8 +111,9 @@ namespace Barotrauma.Items.Components ActivateNearbySleepingCharacters(); reloadTimer = reload; - reloadTimer /= (1f + character.GetStatValue(StatTypes.MeleeAttackSpeed)); - reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier)); + reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed); + reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier); + character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer; item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; @@ -216,6 +217,10 @@ namespace Barotrauma.Items.Components { hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + if (ac.InWater) + { + ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index ddd615402..5086b6899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -20,6 +19,8 @@ namespace Barotrauma.Items.Components private CoroutineHandle pickingCoroutine; + public virtual bool IsAttached => false; + public List AllowedSlots { get { return allowedSlots; } @@ -71,6 +72,7 @@ namespace Barotrauma.Items.Components //return if someone is already trying to pick the item if (pickTimer > 0.0f) { return false; } if (picker == null || picker.Inventory == null) { return false; } + if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; } if (PickingTime > 0.0f) { @@ -226,7 +228,7 @@ namespace Barotrauma.Items.Components { foreach (Connection c in connectionPanel.Connections) { - foreach (Wire w in c.Wires) + foreach (Wire w in c.Wires.ToArray()) { if (w == null) continue; w.Item.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 971068a95..cadea84be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (character == null || character.Removed) return false; + if (character == null || character.Removed) { return false; } if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; } IsActive = true; @@ -55,12 +55,11 @@ namespace Barotrauma.Items.Components if (UsableIn == UseEnvironment.Water) { return true; } } - Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); - //move upwards if the cursor is at the position of the character - if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY; - + Vector2 dir = character.CursorPosition - character.Position; + if (!MathUtils.IsValid(dir)) { return true; } + float length = 200; + dir = dir.ClampLength(length) / length; Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier; - if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; } foreach (Limb limb in character.AnimController.Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 339d218aa..998d7d7c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components { get { - Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? MathHelper.ToRadians(item.Rotation) : item.body.Rotation); + Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? item.RotationRad : item.body.Rotation); Vector2 flippedPos = barrelPos; if (item.body != null && item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } return Vector2.Transform(flippedPos, bodyTransform) * item.Scale; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 7a624ac27..f7b360dad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Can the item hit broken doors.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")] public bool HitItems { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] @@ -697,14 +697,17 @@ namespace Barotrauma.Items.Components Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos; float dist = fromCharacterToLeak.Length(); float reach = AIObjectiveFixLeak.CalculateReach(this, character); - - if (dist > reach * 3) + if (dist > reach * 2) { // Too far away -> consider this done and hope the AI is smart enough to move closer Reset(); return true; } character.AIController.SteeringManager.Reset(); + if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } if (!character.AnimController.InWater) { // TODO: use the collider size? @@ -714,34 +717,25 @@ namespace Barotrauma.Items.Components humanAnim.Crouching = true; } } - if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) + if (!character.IsClimbing) { - // Steer closer - if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) { - // Swimming inside the sub - if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished)) + // Steer closer + Vector2 dir = Vector2.Normalize(fromCharacterToLeak); + if (!character.InWater) { - Vector2 dir = Vector2.Normalize(fromCharacterToLeak); - character.AIController.SteeringManager.SteeringManual(deltaTime, dir); - } - else - { - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + dir.Y = 0; } + character.AIController.SteeringManager.SteeringManual(deltaTime, dir); } - else + else if (dist < reach * 0.25f && !character.IsClimbing) { - // Swimming outside the sub - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + // Too close -> steer away + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } } - else if (dist < reach * 0.25f) - { - // Too close -> steer away - character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); - } - if (dist <= reach) + if (dist <= reach || character.IsClimbing) { // In range character.CursorPosition = leak.WorldPosition; @@ -815,7 +809,7 @@ namespace Barotrauma.Items.Components } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && - (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); + (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f); if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 845ae5efe..a95f15214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -388,7 +388,7 @@ namespace Barotrauma.Items.Components IsActive = isActive; } - public void SetRequiredItems(ContentXElement element) + public void SetRequiredItems(ContentXElement element, bool allowEmpty = false) { bool returnEmpty = false; #if CLIENT @@ -410,13 +410,13 @@ namespace Barotrauma.Items.Components requiredItems[ri.Type].Add(ri); } } - else + else if (!allowEmpty) { DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); } } - public virtual void Move(Vector2 amount) { } + public virtual void Move(Vector2 amount, bool ignoreContacts = false) { } /// a Character has picked the item public virtual bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 891fc3fce..2b0e7db1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -17,18 +17,20 @@ namespace Barotrauma.Items.Components public readonly Item Item; public readonly StatusEffect StatusEffect; public readonly bool ExcludeBroken; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken) + public readonly bool ExcludeFullCondition; + public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) { Item = item; StatusEffect = statusEffect; ExcludeBroken = excludeBroken; + ExcludeFullCondition = excludeFullCondition; } } class SlotRestrictions { public readonly int MaxStackSize; - public readonly List ContainableItems; + public List ContainableItems; public SlotRestrictions(int maxStackSize, List containableItems) { @@ -185,7 +187,12 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - private SlotRestrictions[] slotRestrictions; + private readonly ImmutableArray slotRestrictions; + + readonly List targets = new List(); + + private Vector2 prevContainedItemPositions; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { @@ -235,10 +242,11 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - slotRestrictions = new SlotRestrictions[totalCapacity]; + + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { - slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems)); } int subContainerIndex = capacity; @@ -266,14 +274,37 @@ namespace Barotrauma.Items.Components for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) { - slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems)); } subContainerIndex += subCapacity; } capacity = totalCapacity; + slotRestrictions = newSlotRestrictions.ToImmutableArray(); + System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length); InitProjSpecific(element); } + public void ReloadContainableRestrictions(ContentXElement element) + { + int containableIndex = 0; + foreach (var subElement in element.GetChildElements("containable")) + { + RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); + if (containable == null) + { + DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers."); + continue; + } + ContainableItems[containableIndex] = containable; + containableIndex++; + if (containableIndex >= ContainableItems.Count) { break; } + } + for (int i = 0; i < capacity; i++) + { + slotRestrictions[i].ContainableItems = ContainableItems; + } + } + public int GetMaxStackSize(int slotIndex) { if (slotIndex < 0 || slotIndex >= capacity) @@ -300,7 +331,7 @@ namespace Barotrauma.Items.Components if (!containableItem.MatchesItem(containedItem)) { continue; } foreach (StatusEffect effect in containableItem.statusEffects) { - activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken)); + activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); } } } @@ -315,7 +346,7 @@ namespace Barotrauma.Items.Components IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { SetContainedItemPositions(); } @@ -363,18 +394,21 @@ namespace Barotrauma.Items.Components return false; } - readonly List targets = new List(); - public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) { SpawnAlwaysContainedItems(); + alwaysContainedItemsSpawned = true; } if (item.ParentInventory is CharacterInventory ownerInventory) { - item.SetContainedItemPositions(); + if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f) + { + SetContainedItemPositions(); + prevContainedItemPositions = item.Position; + } if (AutoInject) { @@ -395,7 +429,7 @@ namespace Barotrauma.Items.Components item.body.Enabled && item.body.FarseerBody.Awake) { - item.SetContainedItemPositions(); + SetContainedItemPositions(); } else if (activeContainedItems.Count == 0) { @@ -408,6 +442,7 @@ namespace Barotrauma.Items.Components Item contained = activeContainedItem.Item; if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; } + if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; if (effect.HasTargetType(StatusEffect.TargetType.This)) @@ -569,7 +604,7 @@ namespace Barotrauma.Items.Components transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); if (Math.Abs(item.Rotation) > 0.01f) { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position; transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); @@ -600,7 +635,7 @@ namespace Barotrauma.Items.Components } else { - currentRotation += MathHelper.ToRadians(-item.Rotation); + currentRotation += -item.RotationRad; } int i = 0; @@ -751,7 +786,11 @@ namespace Barotrauma.Items.Components return; } #endif - Inventory.AllItemsMod.ForEach(it => it.Drop(null)); + //if we're unloading the whole sub, no need to drop anything (everything's going to be removed anyway) + if (!Submarine.Unloading) + { + Inventory.AllItemsMod.ForEach(it => it.Drop(null)); + } } public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 296277c9d..84ad8903e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -114,6 +114,20 @@ namespace Barotrauma.Items.Components private set; } = true; + [Serialize(false, IsPropertySaveable.No)] + public bool NonInteractableWhenFlippedX + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.No)] + public bool NonInteractableWhenFlippedY + { + get; + set; + } + public Controller(Item item, ContentXElement element) : base(item, element) { @@ -147,7 +161,7 @@ namespace Barotrauma.Items.Components CancelUsing(user); user = null; } - if (!IsToggle) { IsActive = false; } + if (!IsToggle || item.Connections == null) { IsActive = false; } return; } @@ -462,19 +476,8 @@ namespace Barotrauma.Items.Components { dir = dir == Direction.Left ? Direction.Right : Direction.Left; } - - userPos.X = -UserPos.X; - - for (int i = 0; i < limbPositions.Count; i++) - { - float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X; - - Vector2 flippedPos = - new Vector2( - (item.Rect.Center.X - diff - item.Rect.X) / item.Scale, - limbPositions[i].Position.Y); - limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb); - } + userPos.X = -UserPos.X; + FlipLimbPositions(); } public override void FlipY(bool relativeToSub) @@ -519,6 +522,11 @@ namespace Barotrauma.Items.Components { if (Screen.Selected == GameMain.SubEditorScreen) { + if (item.FlippedX) + { + FlipLimbPositions(); + } + // Don't save flipped positions. foreach (var limbPos in limbPositions) { element.Add(new XElement("limbposition", @@ -526,6 +534,10 @@ namespace Barotrauma.Items.Components new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)), new XAttribute("allowusinglimb", limbPos.AllowUsingLimb))); } + if (item.FlippedX) + { + FlipLimbPositions(); + } } return element; } @@ -558,5 +570,41 @@ namespace Barotrauma.Items.Components } } } + + private void FlipLimbPositions() + { + for (int i = 0; i < limbPositions.Count; i++) + { + float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X; + + Vector2 flippedPos = + new Vector2( + (item.Rect.Center.X - diff - item.Rect.X) / item.Scale, + limbPositions[i].Position.Y); + limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb); + } + } + + public override void OnItemLoaded() + { + if (item.FlippedX && NonInteractableWhenFlippedX) + { + item.NonInteractable = true; + } + else if (item.FlippedY && NonInteractableWhenFlippedY) + { + item.NonInteractable = true; + } + } + + public override void Reset() + { + base.Reset(); + LoadLimbPositions(originalElement); + if (item.FlippedX) + { + FlipLimbPositions(); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index c09526555..c4f04b034 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -231,28 +231,40 @@ namespace Barotrauma.Items.Components if (targetItem == otherItem) { continue; } if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier)) { - user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); - foreach (Character character in Character.GetFriendlyCrew(user)) - { - character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); - } - var geneticMaterial1 = targetItem.GetComponent(); var geneticMaterial2 = otherItem.GetComponent(); if (geneticMaterial1 != null && geneticMaterial2 != null) { - if (geneticMaterial1.Combine(geneticMaterial2, user)) + var result = geneticMaterial1.Combine(geneticMaterial2, user); + if (result == GeneticMaterial.CombineResult.Refined) { inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); Entity.Spawner.AddItemToRemoveQueue(otherItem); } + if (result != GeneticMaterial.CombineResult.None) + { + OnCombinedOrRefined(); + } allowRemove = false; return; } - inputContainer.Inventory.RemoveItem(otherItem); - OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddItemToRemoveQueue(otherItem); + else + { + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); + OnCombinedOrRefined(); + } + } + } + + void OnCombinedOrRefined() + { + user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 889969115..a3588ac5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -161,7 +161,7 @@ namespace Barotrauma.Items.Components hullData.ReceivedWaterAmount = null; if (fromWaterDetector) { - hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); + hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); } foreach (var linked in sourceHull.linkedTo) { @@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components linkedHullData.ReceivedWaterAmount = null; if (fromWaterDetector) { - linkedHullData.ReceivedWaterAmount = Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index bd0e5f161..fc0d44ebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -1,10 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma.Items.Components { @@ -24,7 +23,10 @@ namespace Barotrauma.Items.Components if (value == hijacked) { return; } hijacked = value; #if SERVER - item.CreateServerEvent(this); + if (!Submarine.Unloading) + { + item.CreateServerEvent(this); + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 3a8095561..a04b1eb25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -68,6 +68,8 @@ namespace Barotrauma.Items.Components private const float ConnectedSubUpdateInterval = 1.0f; float connectedSubUpdateTimer; + private double lastReceivedSteeringSignalTime; + public bool AutoPilot { get { return autoPilot; } @@ -312,16 +314,20 @@ namespace Barotrauma.Items.Components } else if (AutoPilot) { - UpdateAutoPilot(deltaTime); - float throttle = 1.0f; - if (controlledSub != null) + //signals override autopilot for a duration of one second + if (lastReceivedSteeringSignalTime < Timing.TotalTime - 1) { - //if the sub is heading in the correct direction, throttle the speed according to the user's skill - //if it's e.g. sinking due to extra water, don't throttle, but allow emptying up the ballast completely - throttle = MathHelper.Clamp(Vector2.Dot(controlledSub.Velocity, TargetVelocity) / 100.0f, 0.0f, 1.0f); + UpdateAutoPilot(deltaTime); + float throttle = 1.0f; + if (controlledSub != null) + { + //if the sub is heading in the correct direction, throttle the speed according to the user's skill + //if it's e.g. sinking due to extra water, don't throttle, but allow emptying up the ballast completely + throttle = MathHelper.Clamp(Vector2.Dot(controlledSub.Velocity, TargetVelocity) / 100.0f, 0.0f, 1.0f); + } + float maxSpeed = MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f; + TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(100.0f, maxSpeed, throttle)); } - float maxSpeed = MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f; - TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(100.0f, maxSpeed, throttle)); } else { @@ -394,8 +400,8 @@ namespace Barotrauma.Items.Components private void IncreaseSkillLevel(Character user, float deltaTime) { if (user?.Info == null) { return; } - // Do not increase the helm skill when "steering" the sub in an outpost level - if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { return; } + // Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck) + if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( @@ -821,6 +827,7 @@ namespace Barotrauma.Items.Components steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f); steeringInput.Y = MathHelper.Clamp(-steeringInput.Y, -100.0f, 100.0f); TargetVelocity = steeringInput; + lastReceivedSteeringSignalTime = Timing.TotalTime; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index ae166ebf5..24a780d6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components @@ -81,6 +80,8 @@ namespace Barotrauma.Items.Components private ItemContainer? container; private float growthTickTimer; + private List? lightComponents; + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; @@ -107,10 +108,14 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); IsActive = true; #if CLIENT - lightComponent = item.GetComponent(); - if (lightComponent != null) + var lights = item.GetComponents(); + if (lights.Any()) { - lightComponent.Light.Enabled = false; + lightComponents = lights.ToList(); + foreach (var light in lightComponents) + { + light.Light.Enabled = false; + } } #endif container = item.GetComponent(); @@ -227,12 +232,17 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); #if CLIENT - if (lightComponent != null) + if (lightComponents != null && lightComponents.Count > 0) { bool hasSeed = false; - foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; } - - lightComponent.Light.Enabled = hasSeed; + foreach (Growable? seed in GrowableSeeds) + { + hasSeed |= seed != null; + } + foreach (var light in lightComponents) + { + light.Light.Enabled = hasSeed; + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index d22c343b5..f0dbf6fcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components get { return poweredList; } } - public static readonly List ChangedConnections = new List(); + public static readonly HashSet ChangedConnections = new HashSet(); public readonly static Dictionary Grids = new Dictionary(); @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components { if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } } - return voltage; + return PowerConsumption <= 0.0f ? 1.0f : voltage; } set { @@ -158,21 +158,13 @@ namespace Barotrauma.Items.Components } } - public bool PoweredByTinkering - { - get - { - if (this is PowerContainer) { return false; } - foreach (Repairable repairable in Item.Repairables) - { - if (repairable.IsTinkering && repairable.TinkeringPowersDevices) - { - return true; - } - } - return false; - } - } + /// + /// Essentially Voltage / MinVoltage (= how much of the minimum required voltage has been satisfied), clamped between 0 and 1. + /// Can be used by status effects or sounds to check if the item has enough power to run + /// + public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f); + + public bool PoweredByTinkering { get; set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")] public bool VulnerableToEMP @@ -449,7 +441,7 @@ namespace Barotrauma.Items.Components #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("GridUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks); sw.Restart(); #endif @@ -606,7 +598,7 @@ namespace Barotrauma.Items.Components #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("PowerUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fe52d7480..7ca7eff7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -231,6 +231,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description:"Enable only if you want to make the projectile ignore collisions with other projectiles when it's shot. Doesn't have any effect, if the item is not set to be damaged by projectiles.")] + public bool IgnoreProjectilesWhileActive + { + get; + set; + } + public Body StickTarget { get; @@ -405,6 +412,10 @@ namespace Barotrauma.Items.Components item.body.CollisionCategories = Physics.CollisionProjectile; item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) + { + item.body.CollidesWith |= Physics.CollisionProjectile; + } IsActive = true; @@ -835,6 +846,10 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) + { + return false; + } // when hitting limbs with piercing ammo, don't lose as much speed if (MaxTargetsToHit > 1) { @@ -954,7 +969,8 @@ namespace Barotrauma.Items.Components { item.body.LinearVelocity *= deflectedSpeedMultiplier; } - else if ( stickJoint == null && StickTarget == null && + else if ( remainingHits <= 0 && + stickJoint == null && StickTarget == null && StickToStructures && target.Body.UserData is Structure || ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 0be5f9561..0d3dd454c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -56,7 +56,8 @@ namespace Barotrauma.Items.Components if (value == qualityLevel) { return; } bool wasInFullCondition = item.IsFullCondition; - qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); + qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); + item.RecalculateConditionValues(); //set the condition to the new max condition if (wasInFullCondition && statValues.ContainsKey(StatType.Condition)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 45d5d3ae4..7f8fc6789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -106,7 +106,25 @@ namespace Barotrauma.Items.Components } } - public bool IsTinkering { get; private set; } = false; + private bool isTinkering; + public bool IsTinkering + { + get { return isTinkering; } + private set + { + if (isTinkering == value) { return; } + isTinkering = value; + + if (tinkeringPowersDevices) + { + foreach (Powered powered in item.GetComponents()) + { + if (powered is PowerContainer) { continue; } + powered.PoweredByTinkering = isTinkering; + } + } + } + } public Character CurrentFixer { get; private set; } private Item currentRepairItem; @@ -584,7 +602,7 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { - if (Level.IsLoadedOutpost) { return false; } + if (Level.IsLoadedFriendlyOutpost) { return false; } if (LastActiveTime > Timing.TotalTime) { return true; } foreach (ItemComponent ic in item.Components) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 6d1c899b3..7f0c5ca8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -160,7 +160,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (source == null || target == null || target.Removed || - (source is Entity sourceEntity && sourceEntity.Removed)) + (source is Entity sourceEntity && sourceEntity.Removed) || + (source is Limb limb && limb.Removed)) { ResetSource(); target = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs new file mode 100644 index 000000000..e98614a3a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class AndComponent : BooleanOperatorComponent + { + public AndComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs >= 2; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs similarity index 90% rename from Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs rename to Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index 22ceb46ec..bd3140234 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - class AndComponent : ItemComponent + abstract class BooleanOperatorComponent : ItemComponent { protected string output, falseOutput; @@ -70,22 +70,25 @@ namespace Barotrauma.Items.Components } } - public AndComponent(Item item, ContentXElement element) + public BooleanOperatorComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; IsActive = true; } - public override void Update(float deltaTime, Camera cam) + protected abstract bool GetOutput(int numTrueInputs); + + public sealed override void Update(float deltaTime, Camera cam) { - bool state = true; + int receivedInputs = 0; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) { state = false; } + if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } timeSinceReceived[i] += deltaTime; } + bool state = GetOutput(receivedInputs); string signalOut = state ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs new file mode 100644 index 000000000..f7208e8e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class OrComponent : BooleanOperatorComponent + { + public OrComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs > 0; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs new file mode 100644 index 000000000..ee299d837 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class XorComponent : BooleanOperatorComponent + { + public XorComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs == 1; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 51fcd38d9..6d3fbc207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -19,11 +20,8 @@ namespace Barotrauma.Items.Components public readonly string Name; public readonly LocalizedString DisplayName; - private readonly Wire[] wires; - public IEnumerable Wires - { - get { return wires; } - } + private readonly HashSet wires; + public IReadOnlyCollection Wires => wires; private readonly Item item; @@ -31,7 +29,7 @@ namespace Barotrauma.Items.Components public readonly List Effects; - public readonly ushort[] wireId; + public readonly List LoadedWireIds; //The grid the connection is a part of public GridInfo Grid; @@ -92,7 +90,7 @@ namespace Barotrauma.Items.Components MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires); MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires); - wires = new Wire[MaxWires]; + wires = new HashSet(); IsOutput = element.Name.ToString() == "output"; Name = element.GetAttributeString("name", IsOutput ? "output" : "input"); @@ -150,23 +148,15 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - wireId = new ushort[MaxWires]; - + LoadedWireIds = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "link": - int index = -1; - for (int i = 0; i < MaxWires; i++) - { - if (wireId[i] < 1) { index = i; } - } - if (index == -1) { break; } - int id = subElement.GetAttributeInt("w", 0); if (id < 0) { id = 0; } - wireId[index] = idRemap.GetOffsetId(id); + if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); } break; case "statuseffect": @@ -180,143 +170,117 @@ namespace Barotrauma.Items.Components public void SetRecipientsDirty() { recipientsDirty = true; + if (IsPower) { Powered.ChangedConnections.Add(this); } } private void RefreshRecipients() { recipients.Clear(); - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) continue; - Connection recipient = wires[i].OtherConnection(this); - if (recipient != null) recipients.Add(recipient); + Connection recipient = wire.OtherConnection(this); + if (recipient != null) { recipients.Add(recipient); } } recipientsDirty = false; } - public int FindEmptyIndex() - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) return i; - } - return -1; - } - - public int FindWireIndex(Wire wire) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == wire) return i; - } - return -1; - } - - public int FindWireIndex(Item wireItem) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null && wireItem == null) return i; - if (wires[i] != null && wires[i].Item == wireItem) return i; - } - return -1; - } + public Wire FindWireByItem(Item it) + => Wires.FirstOrDefault(w => w.Item == it); + public bool WireSlotsAvailable() + => wires.Count < MaxWires; + public bool TryAddLink(Wire wire) { - for (int i = 0; i < MaxWires; i++) + if (wire is null + || wires.Contains(wire) + || !WireSlotsAvailable()) { - if (wires[i] == null) - { - SetWire(i, wire); - return true; - } + return false; } - return false; + wires.Add(wire); + return true; } - public void SetWire(int index, Wire wire) + public void DisconnectWire(Wire wire) { - Wire previousWire = wires[index]; - if (wire != previousWire && previousWire != null) - { - var otherConnection = previousWire.OtherConnection(this); - if (otherConnection != null) - { - //Change the connection grids or flag them for updating - if (IsPower && otherConnection.IsPower && Grid != null) - { - //Check if both connections belong to a larger grid - if (otherConnection.recipients.Count > 1 && recipients.Count > 1) - { - Powered.ChangedConnections.Add(otherConnection); - Powered.ChangedConnections.Add(this); - } - else if (recipients.Count > 1) - { - //This wire was the only one at the other grid - otherConnection.Grid?.RemoveConnection(otherConnection); - otherConnection.Grid = null; - } - else if (otherConnection.recipients.Count > 1) - { - Grid?.RemoveConnection(this); - Grid = null; - } - else if (Grid.Connections.Count == 2) - { - //Delete the grid as these were the only 2 devices - Powered.Grids.Remove(Grid.ID); - Grid = null; - otherConnection.Grid = null; - } - } - otherConnection.recipientsDirty = true; - } - } + if (wire == null || !wires.Contains(wire)) { return; } - wires[index] = wire; + var prevOtherConnection = wire.OtherConnection(this); + if (prevOtherConnection != null) + { + //Change the connection grids or flag them for updating + if (IsPower && prevOtherConnection.IsPower && Grid != null) + { + //Check if both connections belong to a larger grid + if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1) + { + Powered.ChangedConnections.Add(prevOtherConnection); + Powered.ChangedConnections.Add(this); + } + else if (recipients.Count > 1) + { + //This wire was the only one at the other grid + prevOtherConnection.Grid?.RemoveConnection(prevOtherConnection); + prevOtherConnection.Grid = null; + } + else if (prevOtherConnection.recipients.Count > 1) + { + Grid?.RemoveConnection(this); + Grid = null; + } + else if (Grid.Connections.Count == 2) + { + //Delete the grid as these were the only 2 devices + Powered.Grids.Remove(Grid.ID); + Grid = null; + prevOtherConnection.Grid = null; + } + } + prevOtherConnection.recipientsDirty = true; + } + wires.Remove(wire); recipientsDirty = true; - if (wire != null) + } + + public void ConnectWire(Wire wire) + { + if (wire == null || !TryAddLink(wire)) { return; } + ConnectionPanel.DisconnectedWires.Remove(wire); + var otherConnection = wire.OtherConnection(this); + if (otherConnection != null) { - - ConnectionPanel.DisconnectedWires.Remove(wire); - var otherConnection = wire.OtherConnection(this); - if (otherConnection != null) + //Set the other connection grid if a grid exists already + if (Powered.ValidPowerConnection(this, otherConnection)) { - //Set the other connection grid if a grid exists already - if (Powered.ValidPowerConnection(this, otherConnection)) + if (Grid == null && otherConnection.Grid != null) { - if (Grid == null && otherConnection.Grid != null) - { - otherConnection.Grid.AddConnection(this); - Grid = otherConnection.Grid; - } - else if (Grid != null && otherConnection.Grid == null) - { - Grid.AddConnection(otherConnection); - otherConnection.Grid = Grid; - } - else - { - //Flag change so that proper grids can be formed - Powered.ChangedConnections.Add(this); - Powered.ChangedConnections.Add(otherConnection); - } + otherConnection.Grid.AddConnection(this); + Grid = otherConnection.Grid; + } + else if (Grid != null && otherConnection.Grid == null) + { + Grid.AddConnection(otherConnection); + otherConnection.Grid = Grid; + } + else + { + //Flag change so that proper grids can be formed + Powered.ChangedConnections.Add(this); + Powered.ChangedConnections.Add(otherConnection); } - - otherConnection.recipientsDirty = true; } + + otherConnection.recipientsDirty = true; } + recipientsDirty = true; } public void SendSignal(Signal signal) { - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) { continue; } - - Connection recipient = wires[i].OtherConnection(this); + Connection recipient = wire.OtherConnection(this); if (recipient == null) { continue; } if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } @@ -350,35 +314,32 @@ namespace Barotrauma.Items.Components } } - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) continue; - - wires[i].RemoveConnection(this); - wires[i] = null; + wire.RemoveConnection(this); recipientsDirty = true; } + wires.Clear(); } - public void ConnectLinked() + public void InitializeFromLoaded() { - if (wireId == null) return; + if (LoadedWireIds.Count == 0) { return; } - for (int i = 0; i < MaxWires; i++) + for (int i = 0; i < LoadedWireIds.Count; i++) { - if (wireId[i] == 0) { continue; } + if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; } - if (!(Entity.FindEntityByID(wireId[i]) is Item wireItem)) { continue; } - wires[i] = wireItem.GetComponent(); - recipientsDirty = true; - - if (wires[i] != null) + var wire = wireItem.GetComponent(); + if (wire != null && TryAddLink(wire)) { - if (wires[i].Item.body != null) wires[i].Item.body.Enabled = false; - wires[i].Connect(this, false, false); - wires[i].FixNodeEnds(); + if (wire.Item.body != null) wire.Item.body.Enabled = false; + wire.Connect(this, false, false); + wire.FixNodeEnds(); + recipientsDirty = true; } } + LoadedWireIds.Clear(); } @@ -386,19 +347,10 @@ namespace Barotrauma.Items.Components { XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name)); - Array.Sort(wires, delegate (Wire wire1, Wire wire2) + foreach (var wire in wires.OrderBy(w => w.Item.ID)) { - if (wire1 == null) return 1; - if (wire2 == null) return -1; - return wire1.Item.ID.CompareTo(wire2.Item.ID); - }); - - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) continue; - newElement.Add(new XElement("link", - new XAttribute("w", wires[i].Item.ID.ToString()))); + new XAttribute("w", wire.Item.ID.ToString()))); } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index f7f7d38eb..41a02b481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components public bool TemporarilyLocked { - get { return Level.IsLoadedOutpost && item.GetComponent() != null; } + get { return Level.IsLoadedOutpost && (item.GetComponent()?.Docked ?? false); } } //connection panels can't be deactivated externally (by signals or status effects) @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components { foreach (Connection c in Connections) { - c.ConnectLinked(); + c.InitializeFromLoaded(); } if (disconnectedWireIds != null) @@ -286,25 +286,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - if (loadedConnections[i].wireId.Length == Connections[i].wireId.Length) - { - loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0); - } - else - { - //backwards compatibility when maximum number of wires has changed - foreach (ushort id in loadedConnections[i].wireId) - { - for (int j = 0; j < Connections[i].wireId.Length; j++) - { - if (Connections[i].wireId[j] == 0) - { - Connections[i].wireId[j] = id; - break; - } - } - } - } + Connections[i].LoadedWireIds.Clear(); + Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds); } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); @@ -361,10 +344,8 @@ namespace Barotrauma.Items.Components DisconnectedWires.Clear(); foreach (Connection c in Connections) { - foreach (Wire wire in c.Wires) + foreach (Wire wire in c.Wires.ToArray()) { - if (wire == null) { continue; } - if (wire.OtherConnection(c) == null) //wire not connected to anything else { #if CLIENT @@ -408,13 +389,14 @@ namespace Barotrauma.Items.Components foreach (Connection connection in Connections) { + msg.WriteVariableUInt32((uint)connection.Wires.Count); foreach (Wire wire in connection.Wires) { msg.Write(wire?.Item == null ? (ushort)0 : wire.Item.ID); } } - msg.Write((ushort)DisconnectedWires.Count()); + msg.Write((ushort)DisconnectedWires.Count); foreach (Wire disconnectedWire in DisconnectedWires) { msg.Write(disconnectedWire.Item.ID); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 92149904e..2cbdc98fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Globalization; namespace Barotrauma.Items.Components { @@ -34,13 +35,17 @@ namespace Barotrauma.Items.Components public Identifier PropertyName { get; } public bool TargetOnlyParentProperty { get; } - public int NumberInputMin { get; } - public int NumberInputMax { get; } + public string NumberInputMin { get; } + public string NumberInputMax { get; } + public string NumberInputStep { get; } + public int NumberInputDecimalPlaces { get; } public int MaxTextLength { get; } - public const int DefaultNumberInputMin = 0, DefaultNumberInputMax = 99; - public bool IsIntegerInput { get; } + public const string DefaultNumberInputMin = "0", DefaultNumberInputMax = "99", DefaultNumberInputStep = "1"; + public const int DefaultNumberInputDecimalPlaces = 0; + public bool IsNumberInput { get; } + public NumberType? NumberType { get; } public bool HasPropertyName { get; } public bool ShouldSetProperty { get; set; } @@ -60,11 +65,34 @@ namespace Barotrauma.Items.Components ConnectionName = element.GetAttributeString("connection", ""); PropertyName = element.GetAttributeIdentifier("propertyname", ""); TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); - NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); - NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); + NumberInputMin = element.GetAttributeString("min", DefaultNumberInputMin); + NumberInputMax = element.GetAttributeString("max", DefaultNumberInputMax); + NumberInputStep = element.GetAttributeString("step", DefaultNumberInputStep); + NumberInputDecimalPlaces = element.GetAttributeInt("decimalplaces", DefaultNumberInputDecimalPlaces); MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); + HasPropertyName = !PropertyName.IsEmpty; - IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; + if (HasPropertyName) + { + string elementName = element.Name.ToString().ToLowerInvariant(); + IsNumberInput = elementName == "numberinput" || elementName == "integerinput"; // backwards compatibility + if (IsNumberInput) + { + string numberType = element.GetAttributeString("numbertype", string.Empty); + switch (numberType) + { + case "f": + case "float": + NumberType = Barotrauma.NumberType.Float; + break; + case "int": + case "integer": + default: // backwards compatibility + NumberType = Barotrauma.NumberType.Int; + break; + } + } + } if (element.GetAttribute("signal") is XAttribute attribute) { @@ -152,7 +180,8 @@ namespace Barotrauma.Items.Components { case "button": case "textbox": - case "integerinput": + case "integerinput": // backwards compatibility + case "numberinput": var button = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = false @@ -317,6 +346,24 @@ namespace Barotrauma.Items.Components } } + private void ValueChanged(CustomInterfaceElement numberInputElement, float value) + { + if (numberInputElement == null) { return; } + numberInputElement.Signal = value.ToString(); + if (!numberInputElement.TargetOnlyParentProperty) + { + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; } + e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value); + } + } + else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName)) + { + SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value); + } + } + public override void Update(float deltaTime, Camera cam) { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) @@ -341,5 +388,10 @@ namespace Barotrauma.Items.Components signals = customInterfaceElementList.Select(ci => ci.Signal).ToArray(); return base.Save(parentElement); } + + private static bool TryParseFloatInvariantCulture(string s, out float f) + { + return float.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out f); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index df7bc1b0e..fd5ee7f13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { #if CLIENT Light.Position += amount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6cbb632a9..e278206b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -13,12 +13,13 @@ namespace Barotrauma.Items.Components private float updateTimer; + [Flags] public enum TargetType { - Any, - Human, - Monster, - Wall + Human = 1, + Monster = 2, + Wall = 4, + Any = Human | Monster | Wall, } [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -179,6 +180,11 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); } + if (MotionDetected) + { + ApplyStatusEffects(ActionType.OnUse, deltaTime); + } + updateTimer -= deltaTime; if (updateTimer > 0.0f) { return; } @@ -199,8 +205,7 @@ namespace Barotrauma.Items.Components float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - if (item.CurrentHull == null && item.Submarine != null && - (Target == TargetType.Wall || Target == TargetType.Any)) + if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall)) { if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) { @@ -248,7 +253,7 @@ namespace Barotrauma.Items.Components } } - if (Target != TargetType.Wall) + if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Monster)) { foreach (Character c in Character.CharacterList) { @@ -258,14 +263,13 @@ namespace Barotrauma.Items.Components //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - switch (Target) + if (c.IsHuman) { - case TargetType.Human: - if (!c.IsHuman) { continue; } - break; - case TargetType.Monster: - if (c.IsHuman || c.IsPet) { continue; } - break; + if (!Target.HasFlag(TargetType.Human)) { continue; } + } + else if (!c.IsPet) + { + if (!Target.HasFlag(TargetType.Monster)) { continue; } } //do a rough check based on the position of the character's collider first diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs deleted file mode 100644 index 3d3c7ab9d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components -{ - class OrComponent : AndComponent - { - public OrComponent(Item item, ContentXElement element) - : base(item, element) - { - IsActive = true; - } - - public override void Update(float deltaTime, Camera cam) - { - bool state = false; - for (int i = 0; i < timeSinceReceived.Length; i++) - { - if (timeSinceReceived[i] <= timeFrame) { state = true; } - timeSinceReceived[i] += deltaTime; - } - - string signalOut = state ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) - { - //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } - return; - } - - item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 7fc471035..5e70fcf9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -64,6 +64,11 @@ namespace Barotrauma.Items.Components IsActive = true; } + public static int GetWaterPercentage(Hull hull) + { + return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0; + } + public override void Update(float deltaTime, Camera cam) { if (stateSwitchDelay > 0.0f) @@ -103,12 +108,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - int waterPercentage = 0; - //ignore minuscule amounts of water - if (item.CurrentHull.WaterVolume > 1.0f) - { - waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); - } + int waterPercentage = GetWaterPercentage(item.CurrentHull); if (prevSentWaterPercentageValue != waterPercentage || waterPercentageSignal == null) { prevSentWaterPercentageValue = waterPercentage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index c95c55497..1240fb9cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components // = no point in receiving if (!LinkToChat) { - if (signalOutConnection == null || !signalOutConnection.Wires.Any(w => w != null)) + if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 19588f114..ec17bd33b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -143,12 +143,11 @@ namespace Barotrauma.Items.Components { if (connections[i] == null || connections[i].Item != item) { continue; } - foreach (Wire wire in connections[i].Wires) + if (connections[i].Wires.Contains(this)) { - if (wire != this) continue; SetConnectedDirty(); - connections[i].SetWire(connections[i].FindWireIndex(wire), null); + connections[i].DisconnectWire(this); } connections[i] = null; @@ -597,15 +596,16 @@ namespace Barotrauma.Items.Components for (int i = 0; i < 2; i++) { if (connections[i] == null) { continue; } - int wireIndex = connections[i].FindWireIndex(item); - if (wireIndex == -1) { continue; } + + var wire = connections[i].FindWireByItem(item); + if (wire is null) { continue; } #if SERVER if (!connections[i].Item.Removed && (!connections[i].Item.Submarine?.Loading ?? true) && (!Level.Loaded?.Generating ?? true)) { connections[i].Item.CreateServerEvent(connections[i].Item.GetComponent()); } #endif - connections[i].SetWire(wireIndex, null); + connections[i].DisconnectWire(wire); connections[i] = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs deleted file mode 100644 index 71981bb8b..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components -{ - class XorComponent : AndComponent - { - public XorComponent(Item item, ContentXElement element) - : base(item, element) - { - IsActive = true; - } - - public override void Update(float deltaTime, Camera cam) - { - int receivedInputs = 0; - for (int i = 0; i < timeSinceReceived.Length; i++) - { - if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } - timeSinceReceived[i] += deltaTime; - } - - bool state = receivedInputs == 1; - string signalOut = state ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) - { - //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } - return; - } - - item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 582357d8f..e60825beb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -3,9 +3,9 @@ using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Collections.Generic; -using System.Xml.Linq; +using System.Globalization; +using System.Linq; namespace Barotrauma.Items.Components { @@ -13,6 +13,46 @@ namespace Barotrauma.Items.Components { [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)] + public bool DistanceBasedForce { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)] + public bool ForceFluctuation { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)] + private float ForceFluctuationStrength + { + get + { + return forceFluctuationStrength; + } + set + { + forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f); + } + } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)] + private float ForceFluctuationFrequency + { + get + { + return forceFluctuationFrequency; + } + set + { + forceFluctuationFrequency = Math.Max(value, 0.01f); + } + } + [Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)] + private float ForceFluctuationInterval + { + get + { + return forceFluctuationInterval; + } + set + { + forceFluctuationInterval = Math.Max(value, 0.01f); + } + } public PhysicsBody PhysicsBody { get; private set; } private float Radius { get; set; } @@ -39,11 +79,6 @@ namespace Barotrauma.Items.Components private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); private readonly bool triggerOnce; - private readonly bool distanceBasedForce; - private readonly bool forceFluctuation; - private readonly float forceFluctuationStrength; - private readonly float forceFluctuationFrequency; - private readonly float forceFluctuationInterval; private readonly List statusEffectTargets = new List(); /// /// Effects applied to entities inside the trigger @@ -54,6 +89,10 @@ namespace Barotrauma.Items.Components /// private readonly List attacks = new List(); + private float forceFluctuationStrength; + private float forceFluctuationFrequency; + private float forceFluctuationInterval; + public TriggerComponent(Item item, ContentXElement element) : base(item, element) { string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); @@ -62,15 +101,6 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); } triggerOnce = element.GetAttributeBool("triggeronce", false); - distanceBasedForce = element.GetAttributeBool("distancebasedforce", false); - forceFluctuation = element.GetAttributeBool("forcefluctuation", false); - forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f); - forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f); - forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f); - forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f); - forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f); - forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); - string parentDebugName = $"TriggerComponent in {item.Name}"; foreach (var subElement in element.Elements()) { @@ -93,13 +123,11 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f); Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale); - PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f) + PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy)) { - BodyType = BodyType.Static, - CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy), - CollisionCategories = Physics.CollisionWall, UserData = item }; + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.OnCollision += OnCollision; PhysicsBody.FarseerBody.OnSeparation += OnSeparation; @@ -156,14 +184,14 @@ namespace Barotrauma.Items.Components TriggerActive = triggerers.Any(); - if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { ForceFluctuationTimer += deltaTime; - if (ForceFluctuationTimer >= forceFluctuationInterval) + if (ForceFluctuationTimer >= ForceFluctuationInterval) { - float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel); + float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel); float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v); - CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount); + CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount); ForceFluctuationTimer = 0.0f; GameMain.NetworkMember?.CreateEntityEvent(this); } @@ -182,7 +210,7 @@ namespace Barotrauma.Items.Components LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); } - if (Force < 0.01f) + if (Math.Abs(Force) < 0.01f) { // Just ignore very minimal forces continue; @@ -208,21 +236,65 @@ namespace Barotrauma.Items.Components { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } - float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; + float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); if (force.LengthSquared() < 0.01f) { return; } body.ApplyForce(force); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { - base.Move(amount); if (PhysicsBody != null) { - PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + if (ignoreContacts) + { + PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } + else + { + PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } PhysicsBody.Submarine = item.Submarine; } } + + public override void ReceiveSignal(Signal signal, Connection connection) + { + base.ReceiveSignal(signal, connection); + switch (connection.Name) + { + case "set_force": + if (!FloatTryParse(signal, out float force)) { break; } + Force = force; + break; + case "set_distancebasedforce": + if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; } + DistanceBasedForce = distanceBasedForce; + break; + case "set_forcefluctuation": + if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; } + ForceFluctuation = forceFluctuation; + break; + case "set_forcefluctuationstrength": + if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; } + ForceFluctuationStrength = forceFluctuationStrength; + break; + case "set_forcefluctuationfrequency": + if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; } + ForceFluctuationFrequency = forceFluctuationFrequency; + break; + case "set_forcefluctuationinterval": + if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; } + ForceFluctuationInterval = forceFluctuationInterval; + break; + } + + static bool FloatTryParse(Signal signal, out float value) + { + return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value); + } + } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 824c85659..cf6c5cc42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -4,9 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics.Dynamics; @@ -20,8 +18,6 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; private Vector2 transformedBarrelPos; - - private LightComponent lightComponent; private float rotation, targetRotation; @@ -71,6 +67,8 @@ namespace Barotrauma.Items.Components public Character ActiveUser; private float resetActiveUserTimer; + private List lightComponents; + public float Rotation { get { return rotation; } @@ -168,10 +166,13 @@ namespace Barotrauma.Items.Components rotation = (minRotation + maxRotation) / 2; #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = rotation; - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + light.Rotation = rotation; + light.Light.Rotation = -rotation; + } } #endif } @@ -331,27 +332,39 @@ namespace Barotrauma.Items.Components if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } targetRotation = rotation; - FindLightComponent(); UpdateTransformedBarrelPos(); } - private void FindLightComponent() + private void FindLightComponents() { + if (lightComponents != null) + { + // Can't run again, because of reparenting. + return; + } foreach (LightComponent lc in item.GetComponents()) { + // 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) { - lightComponent = lc; - break; + if (lightComponents == null) + { + lightComponents = new List(); + } + lightComponents.Add(lc); } } #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Parent = null; - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + // We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active) + light.Parent = null; + light.Rotation = Rotation - item.RotationRad; + light.Light.Rotation = -rotation; + } } #endif } @@ -428,7 +441,7 @@ namespace Barotrauma.Items.Components if (MathUtils.NearlyEqual(minRotation, maxRotation)) { - UpdateLightComponent(); + UpdateLightComponents(); return; } @@ -452,7 +465,7 @@ namespace Barotrauma.Items.Components } // Do not increase the weapons skill when operating a turret in an outpost level - if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost)) + if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost)) { user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); @@ -509,14 +522,17 @@ namespace Barotrauma.Items.Components aiFindTargetTimer -= deltaTime; } - UpdateLightComponent(); + UpdateLightComponents(); } - private void UpdateLightComponent() + private void UpdateLightComponents() { - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); + foreach (var light in lightComponents) + { + light.Rotation = Rotation - item.RotationRad; + } } } @@ -661,6 +677,7 @@ namespace Barotrauma.Items.Components while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); + if (!batteries.Any()) { break; } float takePower = neededPower / batteries.Count; takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); foreach (PowerContainer battery in batteries) @@ -1151,8 +1168,12 @@ namespace Barotrauma.Items.Components foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.IsDead || !enemy.Enabled) { continue; } + if (character.Submarine != null) + { + if (enemy.Submarine == character.Submarine) { continue; } + if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + } // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } if (HumanAIController.IsFriendly(character, enemy)) { continue; } @@ -1596,21 +1617,24 @@ namespace Barotrauma.Items.Components } break; case "toggle_light": - if (lightComponent != null && signal.value != "0") + if (lightComponents != null && signal.value != "0") { - lightComponent.IsOn = !lightComponent.IsOn; - UpdateLightComponent(); + foreach (var light in lightComponents) + { + light.IsOn = !light.IsOn; + } + UpdateLightComponents(); } break; case "set_light": - if (lightComponent != null) + if (lightComponents != null) { bool shouldBeOn = signal.value != "0"; - if (shouldBeOn != lightComponent.IsOn) + foreach (var light in lightComponents) { - lightComponent.IsOn = shouldBeOn; - UpdateLightComponent(); + light.IsOn = shouldBeOn; } + UpdateLightComponents(); } break; } @@ -1628,7 +1652,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - FindLightComponent(); + FindLightComponents(); targetRotation = rotation; if (!loadedBaseRotation.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index b783a1126..ae4b5b1c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; -using System.Collections.Immutable; using Barotrauma.Abilities; namespace Barotrauma @@ -18,7 +17,6 @@ namespace Barotrauma Beard, Moustache, FaceAttachment, - JobIndicator, Husk, Herpes } @@ -82,7 +80,20 @@ namespace Barotrauma public string Sound { get; private set; } public Point? SheetIndex { get; private set; } - public LightComponent LightComponent { get; set; } + public LightComponent LightComponent => LightComponents?.FirstOrDefault(); + + public List LightComponents + { + get + { + if (_lightComponents == null) + { + _lightComponents = new List(); + } + return _lightComponents; + } + } + private List _lightComponents; public int Variant { get; set; } @@ -116,7 +127,6 @@ namespace Barotrauma case WearableType.Beard: case WearableType.Moustache: case WearableType.FaceAttachment: - case WearableType.JobIndicator: case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; @@ -338,11 +348,14 @@ namespace Barotrauma.Items.Components foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } - wearableSprites[i].LightComponent = new LightComponent(item, lightElement) + wearableSprites[i].LightComponents.Add(new LightComponent(item, lightElement) { Parent = this - }; - item.AddComponent(wearableSprites[i].LightComponent); + }); + foreach (var light in wearableSprites[i].LightComponents) + { + item.AddComponent(light); + } } i++; @@ -413,7 +426,10 @@ namespace Barotrauma.Items.Components IsActive = true; if (wearableSprite.LightComponent != null) { - wearableSprite.LightComponent.ParentBody = equipLimb.body; + foreach (var light in wearableSprite.LightComponents) + { + light.ParentBody = equipLimb.body; + } } limb[i] = equipLimb; @@ -467,7 +483,10 @@ namespace Barotrauma.Items.Components if (wearableSprites[i].LightComponent != null) { - wearableSprites[i].LightComponent.ParentBody = null; + foreach (var light in wearableSprites[i].LightComponents) + { + light.ParentBody = null; + } } equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]); @@ -494,7 +513,6 @@ namespace Barotrauma.Items.Components } item.SetTransform(picker.SimPosition, 0.0f); - item.SetContainedItemPositions(); item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 03f8e5d48..66bc3c1ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -24,13 +24,32 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { public static List ItemList = new List(); + + private static readonly HashSet dangerousItems = new HashSet(); + + public static IReadOnlyCollection DangerousItems { get { return dangerousItems; } } + + private static readonly List repairableItems = new List(); + + /// + /// Items that have one more more Repairable component + /// + public static IReadOnlyCollection RepairableItems => repairableItems; + + private static readonly List cleanableItems = new List(); + + /// + /// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container) + /// + public static IReadOnlyCollection CleanableItems => cleanableItems; + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; private readonly HashSet tags; - private bool isWire, isLogic; + private readonly bool isWire, isLogic; private Hull currentHull; public Hull CurrentHull @@ -115,7 +134,7 @@ namespace Barotrauma private readonly Quality qualityComponent; - private readonly ConcurrentQueue impactQueue = new ConcurrentQueue(); + private ConcurrentQueue impactQueue; //a dictionary containing lists of the status effects in all the components of the item private readonly bool[] hasStatusEffectsOfType; @@ -181,6 +200,7 @@ namespace Barotrauma if (value != container) { container = value; + CheckCleanable(); SetActiveSprite(); } } @@ -262,24 +282,27 @@ namespace Barotrauma } } - private float rotationRad; + public float RotationRad { get; private set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)] public float Rotation { get { - return MathHelper.ToDegrees(rotationRad); + return MathHelper.ToDegrees(RotationRad); } set { if (!Prefab.AllowRotatingInEditor) { return; } - rotationRad = MathHelper.ToRadians(value); + RotationRad = MathHelper.ToRadians(value); #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { SetContainedItemPositions(); - GetComponent()?.SetLightSourceTransform(); + foreach (var light in GetComponents()) + { + light.SetLightSourceTransform(); + } } #endif } @@ -472,9 +495,9 @@ namespace Barotrauma get { return spriteColor; } } - public bool IsFullCondition => MathUtils.NearlyEqual(Condition, MaxCondition); - public float MaxCondition => Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); - public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); + public bool IsFullCondition { get; private set; } + public float MaxCondition { get; private set; } + public float ConditionPercentage { get; private set; } private float offsetOnSelectedMultiplier = 1.0f; @@ -495,7 +518,8 @@ namespace Barotrauma { float prevConditionPercentage = ConditionPercentage; healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); - Condition = MaxCondition * prevConditionPercentage / 100.0f; + condition = MaxCondition * prevConditionPercentage / 100.0f; + RecalculateConditionValues(); } } @@ -505,7 +529,11 @@ namespace Barotrauma public float MaxRepairConditionMultiplier { get => maxRepairConditionMultiplier; - set { maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); } + set + { + maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); + RecalculateConditionValues(); + } } //the default value should be Prefab.Health, but because we can't use it in the attribute, @@ -806,7 +834,9 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition; + condition = MaxCondition = Prefab.Health; + ConditionPercentage = 100.0f; + lastSentCondition = condition; AllowDeconstruct = itemPrefab.AllowDeconstruct; @@ -835,33 +865,35 @@ namespace Barotrauma var rand = new Random(ID); density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble()); } - body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density); - string collisionCategory = subElement.GetAttributeString("collisioncategory", null); + string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null); + + Category collisionCategory = Physics.CollisionItem; + Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit //(we could also do this by making the projectiles and weapons hit CollisionItem //and check if the collision should be ignored in the OnCollision callback, but //that'd make the hit detection more expensive because every item would be included) - body.CollisionCategories = Physics.CollisionCharacter; - body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + collisionCategory = Physics.CollisionCharacter; } - if (collisionCategory != null) + if (collisionCategoryStr != null) { - if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat)) + if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")"); } else { - body.CollisionCategories = cat; + collisionCategory = cat; if (cat.HasFlag(Physics.CollisionCharacter)) { - body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + collisionCategory |= Physics.CollisionProjectile; } } } + body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false); body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f); body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); body.UserData = this; @@ -992,6 +1024,9 @@ namespace Barotrauma InsertToList(); ItemList.Add(this); + if (Prefab.IsDangerous) { dangerousItems.Add(this); } + if (Repairables.Any()) { repairableItems.Add(this); } + CheckCleanable(); DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1000,6 +1035,10 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnSpawn, 1.0f); Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); + RecalculateConditionValues(); +#if CLIENT + Submarine.ForceVisibilityRecheck(); +#endif } partial void InitProjSpecific(); @@ -1082,17 +1121,16 @@ namespace Barotrauma component.OnActiveStateChanged += (bool isActive) => { - bool hasSounds = false; + bool needsSoundUpdate = false; #if CLIENT - hasSounds = component.HasSounds; + needsSoundUpdate = component.NeedsSoundUpdate(); #endif //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it, - //nor status effects, sounds or conditionals that would need to run + //nor sounds or conditionals that would need to run if (!isActive && !component.UpdateWhenInactive && - !hasSounds && + !needsSoundUpdate && component.Parent == null && - (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) && - (component.statusEffectLists == null || !component.statusEffectLists.Any())) + (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any())) { if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); } } @@ -1129,6 +1167,7 @@ namespace Barotrauma drawableComponents.Add(drawable); hasComponentsToDraw = true; #if CLIENT + Submarine.ForceVisibilityRecheck(); cachedVisibleExtents = null; #endif } @@ -1182,7 +1221,6 @@ namespace Barotrauma public void RemoveContained(Item contained) { ownInventory?.RemoveItem(contained); - contained.Container = null; } @@ -1261,12 +1299,28 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific(); - public override void Move(Vector2 amount) + /// + /// Recheck if the item needs to be included in the list of cleanable items + /// + public void CheckCleanable() { - Move(amount, ignoreContacts: false); + var pickable = GetComponent(); + if (pickable != null && !pickable.IsAttached && + Prefab.PreferredContainers.Any() && + (container == null || container.HasTag("allowcleanup"))) + { + if (!cleanableItems.Contains(this)) + { + cleanableItems.Add(this); + } + } + else + { + cleanableItems.Remove(this); + } } - public void Move(Vector2 amount, bool ignoreContacts) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -1289,7 +1343,7 @@ namespace Barotrauma } foreach (ItemComponent ic in components) { - ic.Move(amount); + ic.Move(amount, ignoreContacts); } if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); } @@ -1495,6 +1549,11 @@ namespace Barotrauma public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null) { + if (effect.intervalTimer > 0.0f) + { + effect.intervalTimer -= deltaTime; + return; + } if (!isNetworkEvent && checkCondition) { if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; } @@ -1614,6 +1673,10 @@ namespace Barotrauma bool wasInFullCondition = IsFullCondition; condition = MathHelper.Clamp(value, 0.0f, MaxCondition); + if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; } + + RecalculateConditionValues(); + if (condition == 0.0f && prev > 0.0f) { //Flag connections to be updated as device is broken @@ -1622,6 +1685,7 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif @@ -1675,6 +1739,17 @@ namespace Barotrauma } } + /// + /// Recalculates the item's maximum condition, condition percentage and whether it's in full condition. + /// You generally never need to call this manually - done automatically when any of the factors that affect the values change. + /// + public void RecalculateConditionValues() + { + MaxCondition = Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); + IsFullCondition = MathUtils.NearlyEqual(Condition, MaxCondition); + ConditionPercentage = MathUtils.Percentage(Condition, MaxCondition); + } + private bool IsInWater() { if (CurrentHull == null) { return true; } @@ -1703,12 +1778,8 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - while (impactQueue.TryDequeue(out float impact)) - { - HandleCollision(impact); - } - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true)) +#if SERVER + if (!(Submarine is { Loading: true })) { sendConditionUpdateTimer -= deltaTime; if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) @@ -1716,14 +1787,23 @@ namespace Barotrauma SendPendingNetworkUpdates(); } } +#endif - if (aiTarget != null) + if (!isActive) { return; } + + if (impactQueue != null) + { + while (impactQueue.TryDequeue(out float impact)) + { + HandleCollision(impact); + } + } + + if (aiTarget != null && aiTarget.NeedsUpdate) { aiTarget.Update(deltaTime); } - if (!isActive) { return; } - ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); @@ -1824,7 +1904,10 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) + if (updateableComponents.Count == 0 && + (aiTarget == null || !aiTarget.NeedsUpdate) && + !hasStatusEffectsOfType[(int)ActionType.Always] && + (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1959,7 +2042,9 @@ namespace Barotrauma if (contact.FixtureA.Body == f1.Body) { normal = -normal; } float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + impactQueue ??= new ConcurrentQueue(); impactQueue.Enqueue(impact); + isActive = true; return true; } @@ -1998,7 +2083,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad); + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -2478,11 +2563,9 @@ namespace Barotrauma if (ic.Use(deltaTime, character)) { ic.WasUsed = true; - #if CLIENT - ic.PlaySound(ActionType.OnUse, character); + ic.PlaySound(ActionType.OnUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); if (ic.DeleteOnUse) { remove = true; } @@ -2511,11 +2594,9 @@ namespace Barotrauma if (ic.SecondaryUse(deltaTime, character)) { ic.WasSecondaryUsed = true; - #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); if (ic.DeleteOnUse) { remove = true; } @@ -2662,6 +2743,9 @@ namespace Barotrauma } SetContainedItemPositions(); +#if CLIENT + Submarine.ForceVisibilityRecheck(); +#endif } public void Equip(Character character) @@ -2680,21 +2764,21 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.Unequip(character); } } - public List> GetProperties() + public List<(object obj, SerializableProperty property)> GetProperties() { - List> allProperties = new List>(); + List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); List itemProperties = SerializableProperty.GetProperties(this); foreach (var itemProperty in itemProperties) { - allProperties.Add(new Pair(this, itemProperty)); + allProperties.Add((this, itemProperty)); } foreach (ItemComponent ic in components) { List componentProperties = SerializableProperty.GetProperties(ic); foreach (var componentProperty in componentProperties) { - allProperties.Add(new Pair(ic, componentProperty)); + allProperties.Add((ic, componentProperty)); } } return allProperties; @@ -2708,13 +2792,13 @@ namespace Barotrauma SerializableProperty property = extraData.SerializableProperty; if (property != null) { - var propertyOwner = allProperties.Find(p => p.Second == property); + var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.Write((byte)allProperties.FindIndex(p => p.Second == property)); + msg.Write((byte)allProperties.FindIndex(p => p.property == property)); } - object value = property.GetValue(propertyOwner.First); + object value = property.GetValue(propertyOwner.obj); if (value is string stringVal) { msg.Write(stringVal); @@ -2795,7 +2879,7 @@ namespace Barotrauma } } - private List> GetInGameEditableProperties(bool ignoreConditions = false) + private List<(object obj, SerializableProperty property)> GetInGameEditableProperties(bool ignoreConditions = false) { if (ignoreConditions) { @@ -2804,7 +2888,7 @@ namespace Barotrauma else { return GetProperties() - .Where(ce => ce.Second.GetAttribute().IsEditable(this)) + .Where(ce => ce.property.GetAttribute().IsEditable(this)) .Union(GetProperties()).ToList(); } } @@ -2823,8 +2907,8 @@ namespace Barotrauma } bool allowEditing = true; - object parentObject = allProperties[propertyIndex].First; - SerializableProperty property = allProperties[propertyIndex].Second; + object parentObject = allProperties[propertyIndex].obj; + SerializableProperty property = allProperties[propertyIndex].property; if (inGameEditableOnly && parentObject is ItemComponent ic) { if (!ic.AllowInGameEditing) { allowEditing = false; } @@ -3152,12 +3236,12 @@ namespace Barotrauma { Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale"); oldRelativeOrigin.Y = -oldRelativeOrigin.Y; - oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.rotationRad); + oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.RotationRad); Vector2 oldOrigin = centerPos + oldRelativeOrigin; Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale; relativeOrigin.Y = -relativeOrigin.Y; - relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.rotationRad); + relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.RotationRad); Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin; item.rect.Location -= (origin - oldOrigin).ToPoint(); @@ -3193,11 +3277,20 @@ namespace Barotrauma item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); item.lastSentCondition = item.condition; + item.RecalculateConditionValues(); item.SetActiveSprite(); - if (submarine?.Info.GameVersion != null) + Version savedVersion = submarine?.Info.GameVersion; + if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase)) { - SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion); + //character inventories are loaded from the game session file - use the version number of the saved game session instead of the sub + //(the sub may have already been saved and up-to-date, even though the character inventories aren't) + savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0")); + } + + if (savedVersion != null) + { + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion); } foreach (ItemComponent component in item.components) @@ -3317,7 +3410,7 @@ namespace Barotrauma { ic.ShallowRemove(); } - ItemList.Remove(this); + RemoveFromLists(); if (body != null) { @@ -3375,7 +3468,8 @@ namespace Barotrauma ic.GuiFrame = null; #endif } - ItemList.Remove(this); + + RemoveFromLists(); if (body != null) { @@ -3408,6 +3502,14 @@ namespace Barotrauma RemoveProjSpecific(); } + private void RemoveFromLists() + { + ItemList.Remove(this); + dangerousItems.Remove(this); + repairableItems.Remove(this); + cleanableItems.Remove(this); + } + partial void RemoveProjSpecific(); public static void RemoveByPrefab(ItemPrefab prefab) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 74532ffa6..e97bb6481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -253,6 +253,12 @@ namespace Barotrauma public readonly float MinCondition; public readonly int MinAmount; public readonly int MaxAmount; + // Overrides min and max, if defined. + public readonly int Amount; + public readonly bool CampaignOnly; + public readonly bool NotCampaign; + public readonly bool TransferOnlyOnePerContainer; + public readonly bool AllowTransfersHere = true; public PreferredContainer(XElement element) { @@ -261,21 +267,26 @@ namespace Barotrauma SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); + Amount = element.GetAttributeInt("amount", 0); MaxCondition = element.GetAttributeFloat("maxcondition", 100f); MinCondition = element.GetAttributeFloat("mincondition", 0f); + CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly); + NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign); + TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); + AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); - if (element.Attribute("spawnprobability") == null) + if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 - if (MaxAmount > 0) + if (MaxAmount > 0 || Amount > 0) { SpawnProbability = 1.0f; } } - else if (element.Attribute("minamount") == null && element.Attribute("maxamount") == null) + else if (element.GetAttribute("minamount") == null && element.GetAttribute("maxamount") == null && element.GetAttribute("amount") == null) { //spawn probability defined but amount isn't, assume amount is 1 - MinAmount = MaxAmount = 1; + MinAmount = MaxAmount = Amount = 1; SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); } } @@ -600,6 +611,9 @@ namespace Barotrauma public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool DontTransferBetweenSubs { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1084,7 +1098,7 @@ namespace Barotrauma //legacy support identifier = GenerateLegacyIdentifier(name); } - prefab = Find(p => p is ItemPrefab && p.Identifier == identifier) as ItemPrefab; + Prefabs.TryGet(identifier, out prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1104,12 +1118,13 @@ namespace Barotrauma return prefab; } - public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false) + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false, bool checkTransferConditions = false) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } - if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer))) + if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && + IsContainerPreferred(pc.Primary, targetContainer) && (!checkTransferConditions || CanBeTransferred(item.Prefab.Identifier, pc, targetContainer)))) { return true; } @@ -1132,6 +1147,8 @@ namespace Barotrauma } private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => + pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 46ae8392c..a74a686d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -36,6 +36,11 @@ namespace Barotrauma /// public bool ExcludeBroken { get; private set; } + /// + /// Should full condition (100%) items be excluded + /// + public bool ExcludeFullCondition { get; private set; } + public bool AllowVariants { get; private set; } = true; public RelationType Type @@ -102,14 +107,14 @@ namespace Barotrauma return CheckContained(parentItem); case RelationType.Container: if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && MatchesItem(parentItem.Container); + return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container); case RelationType.Equipped: if (character == null) { return false; } if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } foreach (Item equippedItem in character.HeldItems) { if (equippedItem == null) { continue; } - if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && MatchesItem(equippedItem)) { return true; } + if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; } } break; case RelationType.Picked: @@ -138,8 +143,7 @@ namespace Barotrauma foreach (Item contained in parentItem.ContainedItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } - if ((!ExcludeBroken || contained.Condition > 0.0f) && MatchesItem(contained)) { return true; } - + if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } if (CheckContained(contained)) { return true; } } return false; @@ -153,6 +157,7 @@ namespace Barotrauma new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), new XAttribute("allowvariants", AllowVariants)); @@ -212,12 +217,12 @@ namespace Barotrauma } } - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { ExcludeBroken = element.GetAttributeBool("excludebroken", true), + ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), AllowVariants = element.GetAttributeBool("allowvariants", true) }; string typeStr = element.GetAttributeString("type", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs new file mode 100644 index 000000000..a8c23fb81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -0,0 +1,42 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class StartItem + { + public Identifier Item; + public int Amount; + + public StartItem(XElement element) + { + Item = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Amount = element.GetAttributeInt("amount", 1); + } + } + + /// + /// Additive sets of items spawned only at the start of the game. + /// + internal class StartItemSet : PrefabWithUintIdentifier + { + public readonly static PrefabCollection Sets = new PrefabCollection(); + + public readonly ImmutableArray Items; + + /// + /// The order in which the sets are displayed in menus + /// + public readonly int Order; + + public StartItemSet(ContentXElement element, StartItemsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) + { + Items = element.Elements().Select(e => new StartItem(e!)).ToImmutableArray(); + Order = element.GetAttributeInt("order", 0); + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index b9fc28d5d..c5c646150 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -25,6 +25,7 @@ namespace Barotrauma IEnumerable aliases = null) : base(identifier) { + System.Diagnostics.Debug.Assert(constructor != null); this.constructor = constructor; this.Name = TextManager.Get($"EntityName.{identifier}"); this.Description = TextManager.Get($"EntityDescription.{identifier}"); @@ -35,40 +36,52 @@ namespace Barotrauma this.Aliases = (aliases ?? Enumerable.Empty()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet(); } + public static CoreEntityPrefab HullPrefab { get; private set; } + public static CoreEntityPrefab GapPrefab { get; private set; } + public static CoreEntityPrefab WayPointPrefab { get; private set; } + public static CoreEntityPrefab SpawnPointPrefab { get; private set; } + public static void InitCorePrefabs() { - CoreEntityPrefab ep = new CoreEntityPrefab( + HullPrefab = new CoreEntityPrefab( "hull".ToIdentifier(), - typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + typeof(Hull).GetConstructor(new Type[] { typeof(Rectangle) }), resizeHorizontal: true, resizeVertical: true, linkable: true, allowedLinks: new Identifier[] { "hull".ToIdentifier() }); - Prefabs.Add(ep, false); + Prefabs.Add(HullPrefab, false); - ep = new CoreEntityPrefab( + GapPrefab = new CoreEntityPrefab( "gap".ToIdentifier(), - typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + typeof(Gap).GetConstructor(new Type[] { typeof(Rectangle) }), resizeHorizontal: true, resizeVertical: true); - Prefabs.Add(ep, false); + Prefabs.Add(GapPrefab, false); - ep = new CoreEntityPrefab( + WayPointPrefab = new CoreEntityPrefab( "waypoint".ToIdentifier(), typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); - Prefabs.Add(ep, false); + Prefabs.Add(WayPointPrefab, false); - ep = new CoreEntityPrefab( + SpawnPointPrefab = new CoreEntityPrefab( "spawnpoint".ToIdentifier(), typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); - Prefabs.Add(ep, false); + Prefabs.Add(SpawnPointPrefab, false); } protected override void CreateInstance(Rectangle rect) { - if (constructor == null) return; - object[] lobject = new object[] { this, rect }; - constructor.Invoke(lobject); + if (this == WayPointPrefab || this == SpawnPointPrefab) + { + object[] lobject = new object[] { this, rect }; + constructor.Invoke(lobject); + } + else + { + object[] lobject = new object[] { rect }; + constructor.Invoke(lobject); + } } private bool disposed = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index d6a3357f0..2a961dcd8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -214,7 +214,7 @@ namespace Barotrauma.MapCreatures.Behavior [Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")] public int RootHealth { get; set; } - [Serialize(0.0005f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")] + [Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")] public float HealthRegenPerBranch { get; set; } [Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")] @@ -399,6 +399,7 @@ namespace Barotrauma.MapCreatures.Behavior new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)), new XAttribute("ID", branch.ID), new XAttribute("isroot", branch.IsRoot), + new XAttribute("isrootgrowth", branch.IsRootGrowth), new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("sides", (int)branch.Sides), @@ -457,16 +458,34 @@ namespace Barotrauma.MapCreatures.Behavior foreach ((BallastFloraBranch branch, int parentBranchId) in branches) { - if (parentBranchId > -1 && parentBranchId < Branches.Count) + if (parentBranchId > -1) { - branch.ParentBranch = Branches[parentBranchId]; + var parentBranch = Branches.Find(b => b.ID == parentBranchId); + if (parentBranch == null) + { + DebugConsole.AddWarning($"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}"); + } + else + { + branch.ParentBranch = parentBranch; + } } } + if (root == null) + { + Branches.ForEach(b => b.DisconnectedFromRoot = true); + } + else + { + CheckDisconnectedFromRoot(); + } + void LoadBranch(XElement branchElement, IdRemap idRemap) { Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); bool isRoot = branchElement.GetAttributeBool("isroot", false); + bool isRootGrowth = branchElement.GetAttributeBool("isrootgrowth", false); int flowerConfig = getInt("flowerconfig"); int leafconfig = getInt("leafconfig"); int id = getInt("ID"); @@ -484,7 +503,8 @@ namespace Barotrauma.MapCreatures.Behavior MaxHealth = maxhealth, Sides = (TileSide) sides, BlockedSides = (TileSide) blockedSides, - IsRoot = isRoot + IsRoot = isRoot, + IsRootGrowth = isRootGrowth }; branches.Add((newBranch, parentBranchId)); @@ -649,10 +669,14 @@ namespace Barotrauma.MapCreatures.Behavior toBeRemoved.Clear(); foreach (BallastFloraBranch branch in Branches) { - if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) + if (!branch.IsRoot) { - float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth); - DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); + if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f) + { + float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth; + float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth); + DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); + } } if (branch.Health <= 0.0f) { @@ -767,7 +791,8 @@ namespace Barotrauma.MapCreatures.Behavior MaxHealth = RootHealth, Health = RootHealth, IsRoot = true, - CurrentHull = Parent + CurrentHull = Parent, + ID = CreateID() }; Branches.Add(root); @@ -992,14 +1017,6 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { float damage = amount; - if (damage > 0) - { - damage = Math.Min(damage, branch.Health); - } - else - { - damage = Math.Max(damage, branch.Health - branch.MaxHealth); - } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -1058,6 +1075,14 @@ namespace Barotrauma.MapCreatures.Behavior } } + if (damage > 0) + { + damage = Math.Min(damage, branch.Health); + } + else + { + damage = Math.Max(damage, branch.Health - branch.MaxHealth); + } branch.Health -= damage; #if SERVER @@ -1071,6 +1096,25 @@ namespace Barotrauma.MapCreatures.Behavior } } + private void CheckDisconnectedFromRoot() + { + bool foundDisconnected; + do + { + foundDisconnected = false; + foreach (BallastFloraBranch branch in Branches) + { + if (branch.ParentBranch == null || branch.DisconnectedFromRoot) { continue; } + if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot) + { + branch.DisconnectedFromRoot = true; + foundDisconnected = true; + } + } + } while (foundDisconnected); + + } + public void RemoveBranch(BallastFloraBranch branch) { bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -1081,20 +1125,7 @@ namespace Barotrauma.MapCreatures.Behavior Branches.Remove(branch); branch.Removed = true; - bool foundDisconnected = false; - do - { - foundDisconnected = false; - foreach (BallastFloraBranch otherBranch in Branches) - { - if (otherBranch.ParentBranch == null || otherBranch.DisconnectedFromRoot) { continue; } - if (otherBranch.ParentBranch.Removed || otherBranch.ParentBranch.DisconnectedFromRoot) - { - otherBranch.DisconnectedFromRoot = true; - foundDisconnected = true; - } - } - } while (foundDisconnected); + CheckDisconnectedFromRoot(); bodies.ForEachMod(body => { @@ -1148,7 +1179,7 @@ namespace Barotrauma.MapCreatures.Behavior return; } #if SERVER - if (!wasRemoved) + if (!wasRemoved && Parent != null && !Parent.Removed) { CreateNetworkMessage(new BranchRemoveEventData(branch)); } @@ -1181,7 +1212,10 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + if (!item.Removed && Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + } #endif } @@ -1199,7 +1233,10 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - CreateNetworkMessage(new KillEventData()); + if (Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new KillEventData()); + } #endif } @@ -1220,8 +1257,11 @@ namespace Barotrauma.MapCreatures.Behavior } _entityList.Remove(this); -#if SERVER - CreateNetworkMessage(new RemoveEventData()); +#if SERVER + if (Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new RemoveEventData()); + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index d0c9e6d0a..291737a4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -120,7 +120,7 @@ namespace Barotrauma } } - public Gap(MapEntityPrefab prefab, Rectangle rectangle) + public Gap(Rectangle rectangle) : this(rectangle, Submarine.MainSub) { #if CLIENT @@ -136,7 +136,7 @@ namespace Barotrauma { } public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) - : base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id) + : base(CoreEntityPrefab.GapPrefab, submarine, id) { this.rect = rect; flowForce = Vector2.Zero; @@ -148,11 +148,12 @@ namespace Barotrauma InsertToList(); float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; - outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize); + outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionCharacter, + findNewContacts: false); outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; - outsideCollisionBlocker.BodyType = BodyType.Static; - outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; - outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; @@ -165,7 +166,7 @@ namespace Barotrauma return new Gap(rect, IsHorizontal, Submarine); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -326,14 +327,6 @@ namespace Barotrauma { lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); } - if (FlowTargetHull != null && IsRoomToRoom) - { - var otherRoom = linkedTo[1] == FlowTargetHull ? linkedTo[0] : linkedTo[1]; - if ((otherRoom as Hull).Volume < FlowTargetHull.Volume) - { - lerpedFlowForce = Vector2.Zero; - } - } openedTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index f4172ff8f..9702e90be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -410,8 +410,8 @@ namespace Barotrauma public BallastFloraBehavior BallastFlora { get; set; } - public Hull(MapEntityPrefab prefab, Rectangle rectangle) - : this (prefab, rectangle, Submarine.MainSub) + public Hull(Rectangle rectangle) + : this (rectangle, Submarine.MainSub) { #if CLIENT if (SubEditorScreen.IsSubEditor()) @@ -421,8 +421,8 @@ namespace Barotrauma #endif } - public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) - : base (prefab, submarine, id) + public Hull(Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) + : base (CoreEntityPrefab.HullPrefab, submarine, id) { rect = rectangle; @@ -500,7 +500,7 @@ namespace Barotrauma public override MapEntity Clone() { - var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine); + var clone = new Hull(rect, Submarine); foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } @@ -590,7 +590,7 @@ namespace Barotrauma return index; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -1543,7 +1543,7 @@ namespace Barotrauma int.Parse(element.GetAttribute("height").Value)); } - var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element)) + var hull = new Hull(rect, submarine, idRemap.GetOffsetId(element)) { WaterVolume = element.GetAttributeFloat("pressure", 0.0f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 227f59f3d..574115e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -10,12 +10,11 @@ using System.Net; namespace Barotrauma { + #warning TODO: MapEntityPrefab should be constrained further to not include item assemblies, as assemblies are effectively not entities at all partial class ItemAssemblyPrefab : MapEntityPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); - private readonly XElement configElement; public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 1205fa136..298638c46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Xml.Linq; namespace Barotrauma { @@ -14,6 +11,8 @@ namespace Barotrauma public readonly LocalizedString Description; public readonly bool IsEndBiome; + public readonly float MinDifficulty; + public readonly float MaxDifficulty; public readonly ImmutableHashSet AllowedZones; @@ -30,8 +29,9 @@ namespace Barotrauma element.GetAttributeString("description", "")); IsEndBiome = element.GetAttributeBool("endbiome", false); - AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); + MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); + MaxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); } public static Identifier ParseIdentifier(ContentXElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index 4adb2834f..b71326832 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -96,24 +96,31 @@ namespace Barotrauma public readonly Sprite WallSprite; public readonly Sprite WallEdgeSprite; - public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) + public static CaveGenerationParams GetRandom(Level level, bool abyss, Rand.RandSync rand) { var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList(); - if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) + if (caveParams.All(p => p.GetCommonness(level.LevelData, abyss) <= 0.0f)) { return caveParams.First(); } - return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); + return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(level.LevelData, abyss)).ToList(), rand); } - public float GetCommonness(LevelGenerationParams generationParams, bool abyss) + public float GetCommonness(LevelData levelData, bool abyss) { - if (generationParams != null && - generationParams.Identifier != Identifier.Empty && - OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness)) + if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : levelData.GenerationParams.Identifier, out float commonness)) { return commonness; } + if (levelData?.Biome != null) + { + if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness)) + { + return biomeCommonness; + } + } + return Commonness; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 735066d2d..133da33e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -157,9 +157,6 @@ namespace Barotrauma public static List GeneratePath(List targetCells, List cells) { - Stopwatch sw2 = new Stopwatch(); - sw2.Start(); - List pathCells = new List(); if (targetCells.Count == 0) { return pathCells; } @@ -213,10 +210,6 @@ namespace Barotrauma } while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0); - - Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms"); - sw2.Restart(); - return pathCells; } @@ -351,7 +344,7 @@ namespace Barotrauma BodyType = BodyType.Static, CollisionCategories = Physics.CollisionLevel }; - GameMain.World.Add(cellBody); + GameMain.World.Add(cellBody, findNewContacts: false); for (int n = cells.Count - 1; n >= 0; n-- ) { @@ -429,7 +422,9 @@ namespace Barotrauma Vertices bodyVertices = new Vertices(triangles[i]); PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f); - Fixture fixture = new Fixture(polygon) + Fixture fixture = new Fixture(polygon, + Physics.CollisionLevel, + Physics.CollisionAll) { UserData = cell }; @@ -446,8 +441,6 @@ namespace Barotrauma } cell.Body = cellBody; } - - cellBody.CollisionCategories = Physics.CollisionLevel; cellBody.ResetMassData(); return cellBody; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 763fb0324..4dd4ad9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -49,7 +49,7 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20, // Not used anywhere + BeaconStation = 0x20, Abyss = 0x40, AbyssCave = 0x80 } @@ -299,11 +299,50 @@ namespace Barotrauma /// Random integers generated during the level generation. If these values differ between clients/server, /// it means the levels aren't identical for some reason and there will most likely be major ID mismatches. /// - public List EqualityCheckValues + public enum LevelGenStage { - get; - private set; - } = new List(); + LevelGenParams, + Size, + GenStart, + TunnelGen, + AbyssGen, + CaveGen, + VoronoiGen, + VoronoiGen2, + VoronoiGen3, + Ruins, + FloatingIce, + LevelBodies, + IceSpires, + TopAndBottom, + PlaceLevelObjects, + GenerateItems, + Finish + } + + private readonly Dictionary equalityCheckValues = Enum.GetValues(typeof(LevelGenStage)) + .Cast() + .Select(k => (k, 0)) + .ToDictionary(); + public IReadOnlyDictionary EqualityCheckValues => equalityCheckValues; + + private void GenerateEqualityCheckValue(LevelGenStage stage) + { + equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient); + } + + private void SetEqualityCheckValue(LevelGenStage stage, int value) + { + equalityCheckValues[stage] = value; + } + + private void ClearEqualityCheckValues() + { + foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage))) + { + equalityCheckValues[stage] = 0; + } + } public List EntitiesBeforeGenerate { get; private set; } = new List(); public int EntityCountBeforeGenerate { get; private set; } @@ -356,6 +395,13 @@ 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) + /// + public static bool IsLoadedFriendlyOutpost => + loaded?.Type == LevelData.LevelType.Outpost && + (loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1); + public LevelGenerationParams GenerationParams { get { return LevelData.GenerationParams; } @@ -382,7 +428,7 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } - public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); } @@ -394,27 +440,35 @@ namespace Barotrauma preSelectedStartOutpost = startOutpost, preSelectedEndOutpost = endOutpost }; - level.Generate(mirror); + level.Generate(mirror, startLocation, endLocation); return level; } - private void Generate(bool mirror) + private void Generate(bool mirror, Location startLocation, Location endLocation) { Loaded?.Remove(); Loaded = this; Generating = true; +#if CLIENT + Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode); +#endif + Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier)); + DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier); - EqualityCheckValues.Clear(); + ClearEqualityCheckValues(); EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); if (LevelData.ForceOutpostGenerationParams == null) { - StartLocation = GameMain.GameSession?.StartLocation; - EndLocation = GameMain.GameSession?.EndLocation; + StartLocation = startLocation; + EndLocation = endLocation; } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.GenStart); + SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); + SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen); LevelObjectManager = new LevelObjectManager(); @@ -462,7 +516,7 @@ namespace Barotrauma Rectangle pathBorders = borders; pathBorders.Inflate( -Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5), - -Math.Min(minMainPathWidth, borders.Height / 5)); + -Math.Min(minMainPathWidth * 2, borders.Height / 5)); if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); } if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); } @@ -477,7 +531,7 @@ namespace Barotrauma (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); endExitPosition = new Point(endPosition.X, borders.Bottom); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -552,10 +606,12 @@ namespace Barotrauma } int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient); + for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel); + Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } @@ -570,10 +626,16 @@ namespace Barotrauma CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); + + GenerateEqualityCheckValue(LevelGenStage.AbyssGen); + GenerateAbyssArea(); + + GenerateEqualityCheckValue(LevelGenStage.CaveGen); + GenerateCaves(mainPath); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -678,7 +740,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -796,7 +858,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -1025,7 +1087,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.Ruins); //---------------------------------------------------------------------------------- // create some ruins @@ -1038,7 +1100,7 @@ namespace Barotrauma GenerateRuin(ruinPositions[i], mirror); } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.FloatingIce); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1070,7 +1132,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.LevelBodies); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1175,7 +1237,7 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.IceSpires); //---------------------------------------------------------------------------------- // create ice spires @@ -1210,7 +1272,7 @@ namespace Barotrauma CreateOutposts(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1252,15 +1314,15 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.GenerateItems); GenerateItems(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.Finish); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -1658,10 +1720,11 @@ namespace Barotrauma #endif } } - else + else if (abyssHeight > 30000) { //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth - if (abyssEndY + CrushDepth < 0) + //but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth) + if (abyssEndY + CrushDepth < 0 && abyssStartY > -CrushDepth) { abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2); } @@ -1770,7 +1833,7 @@ namespace Barotrauma } } - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient); + var caveParams = CaveGenerationParams.GetRandom(this, abyss: true, rand: Rand.RandSync.ServerAndClient); float caveScaleRelativeToIsland = 0.7f; GenerateCave( @@ -1839,7 +1902,7 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient); + var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient); Point caveSize = new Point( Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient), Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient)); @@ -2429,6 +2492,7 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier)) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || + itemPrefab.LevelCommonness.TryGetValue(LevelData.Biome.Identifier, out commonness) || itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness)) { if (commonness <= 0.0f) { continue; } @@ -2606,7 +2670,7 @@ namespace Barotrauma #if DEBUG DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" + " Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" + - " Total value: "+ PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))+" mk"); + " Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk"); if (AbyssResources.Count > 0) { @@ -3187,7 +3251,8 @@ namespace Barotrauma if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); - Debug.Assert(t <= 1.0f); + //t can go slightly outside the 0-1 due to rounding, safe to ignore + Debug.Assert(t <= 1.001f && t >= -0.001f); t = MathHelper.Clamp(t, 0.0f, 1.0f); float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); @@ -3469,6 +3534,8 @@ namespace Barotrauma } else if (type == SubmarineType.BeaconStation) { + PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub)); + sub.ShowSonarMarker = false; sub.DockedTo.ForEach(s => s.ShowSonarMarker = false); sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; @@ -3688,6 +3755,7 @@ namespace Barotrauma if (wreckFiles.None()) { DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + Wrecks = new List(); return; } wreckFiles.Shuffle(Rand.RandSync.ServerAndClient); @@ -3881,7 +3949,7 @@ namespace Barotrauma //the submarine port has to be at the top of the sub if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; } float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X); - if (dist < closestDistance) + if (dist < closestDistance || subPort.MainDockingPort) { subPort = port; closestDistance = dist; @@ -3964,11 +4032,35 @@ namespace Barotrauma DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); return; } + + var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); - if (BeaconStation == null) { return; } + if (BeaconStation == null) + { + LevelData.HasBeaconStation = false; + return; + } Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent() != null); if (sonarItem == null) @@ -3984,6 +4076,11 @@ namespace Barotrauma if (!LevelData.HasBeaconStation) { return; } if (GameMain.NetworkMember?.IsClient ?? false) { return; } + if (BeaconStation == null) + { + throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level)."); + } + List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); Item reactorItem = beaconItems.Find(it => it.GetComponent() != null); @@ -4019,24 +4116,22 @@ namespace Barotrauma { if (!(GameMain.NetworkMember?.IsClient ?? false)) { - //empty the reactor - if (reactorContainer != null) + bool allowDisconnectedWires = true; + bool allowDamagedWalls = true; + if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) { - foreach (Item item in reactorContainer.Inventory.AllItems) - { - if (item.NonInteractable) { continue; } - Spawner.AddItemToRemoveQueue(item); - } + allowDisconnectedWires = info.AllowDisconnectedWires; + allowDamagedWalls = info.AllowDamagedWalls; } //remove wires float removeWireMinDifficulty = 20.0f; float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f) + if (removeWireProbability > 0.0f && allowDisconnectedWires) { foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - if (item.NonInteractable) { continue; } + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } Wire wire = item.GetComponent(); if (wire.Locked) { continue; } if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) @@ -4056,8 +4151,8 @@ namespace Barotrauma connection.ConnectionPanel.DisconnectedWires.Add(wire); wire.RemoveConnection(connection.Item); #if SERVER - connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); - wire.CreateNetworkEvent(); + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); #endif } } @@ -4065,23 +4160,25 @@ namespace Barotrauma } } - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + if (allowDamagedWalls) { - if (item.NonInteractable) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + { + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + } } - } - - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) - { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + { + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + } } } } @@ -4112,12 +4209,12 @@ namespace Barotrauma int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1); var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); - pathPoints.Shuffle(Rand.RandSync.Unsynced); var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); - corpsePoints.Shuffle(Rand.RandSync.Unsynced); - if (!corpsePoints.Any() && !pathPoints.Any()) { continue; } - + pathPoints.Shuffle(Rand.RandSync.Unsynced); + // Sort by job so that we first spawn those with a predefined job (might have special id cards) + corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList(); + var usedJobs = new HashSet(); int spawnCounter = 0; for (int j = 0; j < corpseCount; j++) { @@ -4126,18 +4223,18 @@ namespace Barotrauma CorpsePrefab selectedPrefab; if (job == null) { - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + selectedPrefab = GetCorpsePrefab(usedJobs); } else { - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier)); + selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier); if (selectedPrefab == null) { corpsePoints.Remove(sp); pathPoints.Remove(sp); sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + selectedPrefab = GetCorpsePrefab(usedJobs); } } if (selectedPrefab == null) { continue; } @@ -4156,28 +4253,65 @@ namespace Barotrauma pathPoints.Remove(sp); } - job ??= selectedPrefab.GetJobPrefab(); + job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p)); if (job == null) { continue; } - + if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer") + { + // Only spawn one of these jobs per wreck + usedJobs.Add(job); + } var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient); var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); + corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); + bool applyBurns = Rand.Value() < 0.1f; + bool applyDamage = Rand.Value() < 0.3f; + foreach (var limb in corpse.AnimController.Limbs) + { + if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f)) + { + var prefab = AfflictionPrefab.BiteWounds; + float max = prefab.MaxStrength / prefab.DamageOverlayAlpha; + corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max))); + } + if (applyBurns) + { + var prefab = AfflictionPrefab.Burn; + float max = prefab.MaxStrength / prefab.BurnOverlayAlpha; + corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max))); + } + + static float GetStrength(Limb limb, float max) + { + float strength = Rand.Range(0, max); + if (limb.type != LimbType.Head) + { + strength = Math.Min(strength, Rand.Range(0, max)); + } + return strength; + } + } corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.GiveIdCardTags(sp); -#if SERVER - if (selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) + + bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) { corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced)); } -#endif + spawnCounter++; - static CorpsePrefab GetCorpsePrefab(Func predicate) + static CorpsePrefab GetCorpsePrefab(HashSet usedJobs, Func predicate = null) { - IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate); + IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(p => + usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) && + p.SpawnPosition == PositionType.Wreck && + (predicate == null || predicate(p))); + return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced); } } @@ -4270,7 +4404,7 @@ namespace Barotrauma blockedRects?.Clear(); EntitiesBeforeGenerate?.Clear(); - EqualityCheckValues?.Clear(); + ClearEqualityCheckValues(); if (Ruins != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 8356a9461..a6de99b67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -20,7 +20,7 @@ namespace Barotrauma public readonly string Seed; - public float Difficulty; + public readonly float Difficulty; public readonly Biome Biome; @@ -90,10 +90,10 @@ namespace Barotrauma (int)MathUtils.Round(generationParams.Height, Level.GridCellSize)); } - public LevelData(XElement element) + public LevelData(XElement element, float? forceDifficulty = null) { Seed = element.GetAttributeString("seed", ""); - Difficulty = element.GetAttributeFloat("difficulty", 0.0f); + Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); Size = element.GetAttributePoint("size", new Point(1000)); Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type); @@ -141,8 +141,8 @@ namespace Barotrauma Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier); Difficulty = locationConnection.Difficulty; + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier); float sizeFactor = MathUtils.InverseLerp( MapGenerationParams.Instance.SmallLevelConnectionLength, @@ -171,13 +171,13 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location) + public LevelData(Location location, float difficulty) { Seed = location.BaseName; Biome = location.Biome; Type = LevelType.Outpost; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier); - Difficulty = 0.0f; + Difficulty = difficulty; + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Difficulty, Biome.Identifier); var rand = new MTRandom(ToolBox.StringToInt(Seed)); int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble()); @@ -200,14 +200,16 @@ namespace Barotrauma (requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) : generationParams.Type; - if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } + float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient); + + if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); } var biome = Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ?? Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); var levelData = new LevelData( seed, - difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient), + selectedDifficulty, Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient), generationParams, biome); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index cb6640f49..3742ade58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -68,6 +67,27 @@ namespace Barotrauma set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float Commonness + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be above or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float MinLevelDifficulty + { + get; + set; + } + + [Serialize(100.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be below or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float MaxLevelDifficulty + { + get; + set; + } + [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] public Color AmbientLightColor { @@ -394,7 +414,7 @@ namespace Barotrauma set; } - [Serialize(50, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(40, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int AbyssResourceClustersMax { get; @@ -536,7 +556,26 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default) + #warning TODO: this should be in the unit test project (#3164) + public static void CheckValidity() + { + foreach (Biome biome in Biome.Prefabs) + { + for (float i = 0.0f; i <= 100.0f; i += 0.5f) + { + if (GetRandom("test", LevelData.LevelType.LocationConnection, i, biome.Identifier) == null) + { + DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: LocationConnection, difficulty: {i}, biome: {biome.Identifier})"); + } + if (GetRandom("test", LevelData.LevelType.Outpost, i, biome.Identifier) == null) + { + DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: Outpost, difficulty: {i}, biome: {biome.Identifier})"); + } + } + } + } + + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -545,7 +584,9 @@ namespace Barotrauma throw new InvalidOperationException("Level generation presets not found - using default presets"); } - var matchingLevelParams = LevelParams.Where(lp => + var levelParamsOrdered = LevelParams.OrderBy(l => l.UintIdentifier); + + var matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); @@ -559,16 +600,25 @@ namespace Barotrauma if (!biome.IsEmpty) { //try to find params that at least have a suitable type - matchingLevelParams = LevelParams.Where(lp => lp.Type == type); + matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); if (!matchingLevelParams.Any()) { //still not found, give up and choose some params randomly - matchingLevelParams = LevelParams; + matchingLevelParams = levelParamsOrdered; } } } - return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient); + if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) + { + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty); + } + + return ToolBox.SelectWeightedRandom(matchingLevelParams, p => p.Commonness, Rand.RandSync.ServerAndClient); } public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 5fd728e3e..8e30125de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -102,32 +102,44 @@ namespace Barotrauma foreach (Structure structure in Structure.WallList) { if (!structure.HasBody || structure.HiddenInGame) { continue; } + + LevelObjectPrefab.SpawnPosType spawnPosType = LevelObjectPrefab.SpawnPosType.None; if (level.Ruins.Any(r => r.Submarine == structure.Submarine)) { - if (structure.IsHorizontal) - { - bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; - bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; - if (topHull && bottomHull ) { continue; } + spawnPosType = LevelObjectPrefab.SpawnPosType.RuinWall; + } + else if (structure.Submarine?.Info?.Type == SubmarineType.Outpost) + { + spawnPosType = LevelObjectPrefab.SpawnPosType.OutpostWall; + } + else + { + continue; + } - availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), - bottomHull ? Vector2.UnitY : -Vector2.UnitY, - LevelObjectPrefab.SpawnPosType.RuinWall, - bottomHull ? Alignment.Bottom : Alignment.Top)); - } - else - { - bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; - bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; - if (rightHull && leftHull) { continue; } + if (structure.IsHorizontal) + { + bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; + bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; + if (topHull && bottomHull) { continue; } - availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), - leftHull ? Vector2.UnitX : -Vector2.UnitX, - LevelObjectPrefab.SpawnPosType.RuinWall, - leftHull ? Alignment.Left : Alignment.Right)); - } + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), + bottomHull ? Vector2.UnitY : -Vector2.UnitY, + spawnPosType, + bottomHull ? Alignment.Bottom : Alignment.Top)); + } + else + { + bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; + bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; + if (rightHull && leftHull) { continue; } + + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), + leftHull ? Vector2.UnitX : -Vector2.UnitX, + spawnPosType, + leftHull ? Alignment.Left : Alignment.Right)); } } @@ -158,7 +170,7 @@ namespace Barotrauma for (int i = 0; i < amount; i++) { //get a random prefab and find a place to spawn it - LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams, availablePrefabs); + LevelObjectPrefab prefab = GetRandomPrefab(level, availablePrefabs); if (prefab == null) { continue; } if (!suitableSpawnPositions.ContainsKey(prefab)) { @@ -583,12 +595,12 @@ namespace Barotrauma } } - private LevelObjectPrefab GetRandomPrefab(LevelGenerationParams generationParams, IList availablePrefabs) + private LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) { - if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; } + if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient); + availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient); } private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 9917943c1..a2ad86140 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -44,6 +44,7 @@ namespace Barotrauma MainPath = 64, LevelStart = 128, LevelEnd = 256, + OutpostWall = 512, Wall = MainPathWall | SidePathWall | CaveWall, } @@ -425,15 +426,21 @@ namespace Barotrauma return requireCaveSpecificOverride ? 0.0f : Commonness; } - public float GetCommonness(LevelGenerationParams generationParams) - { - if (generationParams != null && - generationParams.Identifier != Identifier.Empty && - (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + public float GetCommonness(LevelData levelData) + { + if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(levelData.GenerationParams.Identifier, out float commonness) || + (!levelData.GenerationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(levelData.GenerationParams.OldIdentifier, out commonness))) { return commonness; } + if (levelData?.Biome != null) + { + if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness)) + { + return biomeCommonness; + } + } return Commonness; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 02140f9a5..480163ffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 67cb59004..8b3c2c297 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -152,12 +152,6 @@ namespace Barotrauma public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { #if CLIENT VertexBuffer?.Dispose(); VertexBuffer = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 0ace84cde..da8d88b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -1,12 +1,11 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.IO; -using Barotrauma.Extensions; -using System.Collections.Immutable; namespace Barotrauma { @@ -82,6 +81,8 @@ namespace Barotrauma private XElement saveElement; + private Vector2? positionRelativeToMainSub; + public override bool Linkable { get @@ -215,16 +216,19 @@ namespace Barotrauma saveElement = element }; - if (!string.IsNullOrWhiteSpace(levelSeed) && levelData != null && - levelData.Seed != levelSeed && !linkedSub.purchasedLostShuttles) - { - linkedSub.loadSub = false; - } - else + bool levelMatches = string.IsNullOrWhiteSpace(levelSeed) || levelData == null || levelData.Seed == levelSeed; + + //don't load a sub that was left in this level if we have a submarine switch pending + //to make sure it gets ignored during the submarine switch and item transfer (reloading and saving it during the switch makes it not considered "left behind") + if ((levelMatches || linkedSub.purchasedLostShuttles) && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) { linkedSub.loadSub = true; linkedSub.rect.Location = MathUtils.ToPoint(pos); } + else + { + linkedSub.loadSub = false; + } } #warning TODO: revise @@ -253,6 +257,15 @@ namespace Barotrauma } } + public void SetPositionRelativeToMainSub() + { + if (positionRelativeToMainSub.HasValue) + { + Sub.SetPosition(Submarine.WorldPosition + positionRelativeToMainSub.Value); + } + positionRelativeToMainSub = null; + } + public override void OnMapLoaded() { if (!loadSub) { return; } @@ -279,14 +292,14 @@ namespace Barotrauma if (worldPos != Vector2.Zero) { if (GameMain.GameSession != null && GameMain.GameSession.MirrorLevel) - { + { worldPos.X = GameMain.GameSession.LevelData.Size.X - worldPos.X; } sub.SetPosition(worldPos); } else { - sub.SetPosition(WorldPosition); + sub.SetPosition(WorldPosition); } DockingPort linkedPort = null; @@ -308,8 +321,29 @@ namespace Barotrauma { linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent(); } - if (linkedPort == null) { return; } } + + if (linkedPort == null) + { + if (worldPos == Vector2.Zero) + { + Vector2 relativePos = saveElement.GetAttributeVector2("posrelativetomainsub", Vector2.Zero); + if (relativePos != Vector2.Zero) + { + positionRelativeToMainSub = relativePos; + } + else + { + DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include a world position, a linked port or position relative to the main sub."); + } + } + else + { + sub.Submarine = Submarine; + } + return; + } + originalLinkedPort = linkedPort; ushort originalMyId = childRemap.GetOffsetId(originalMyPortID); @@ -432,7 +466,7 @@ namespace Barotrauma if (sub != null) { bool leaveBehind = false; - if (!sub.DockedTo.Contains(Submarine.MainSub)) + if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); if (Submarine.MainSub.AtEndExit) @@ -457,8 +491,9 @@ namespace Barotrauma } else { - if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove(); - if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove(); + if (saveElement.Attribute("location") != null) { saveElement.Attribute("location").Remove(); } + if (saveElement.Attribute("worldpos") != null) { saveElement.Attribute("worldpos").Remove(); } + saveElement.SetAttributeValue("posrelativetomainsub", XMLExtensions.Vector2ToString(sub.WorldPosition - Submarine.WorldPosition)); } saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index f39a30ee4..08ac7d07d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; -using StoreBalanceStatus = Barotrauma.LocationType.StoreBalanceStatus; namespace Barotrauma { @@ -92,21 +91,8 @@ namespace Barotrauma public class StoreInfo { - private int balance; - public Identifier Identifier { get; } - public int Balance - { - get - { - return balance; - } - set - { - balance = value; - ActiveBalanceStatus = Location.GetStoreBalanceStatus(value); - } - } + public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); public List RequestedGoods { get; } = new List(); @@ -114,8 +100,6 @@ namespace Barotrauma /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. /// public int PriceModifier { get; set; } - public StoreBalanceStatus ActiveBalanceStatus { get; private set; } - public Color BalanceColor => ActiveBalanceStatus.Color; public Location Location { get; } private StoreInfo(Location location) @@ -298,14 +282,7 @@ namespace Barotrauma price = Location.DailySpecialPriceModifier * price; } // Adjust by current location reputation - if (Location.Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; - } + price *= Location.GetStoreReputationModifier(true); // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -319,22 +296,13 @@ namespace Barotrauma float price = Location.StoreSellPriceModifier * priceInfo.Price; // Adjust by random price modifier price = (100 - PriceModifier) / 100.0f * price; - // Adjust by current store balance - price = ActiveBalanceStatus.SellPriceModifier * price; // Adjust by requested good status if (considerRequestedGoods && RequestedGoods.Contains(item)) { price = Location.RequestGoodPriceModifier * price; } // Adjust by current location reputation - if (Location.Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; - } + price *= Location.GetStoreReputationModifier(false); // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -353,7 +321,6 @@ namespace Barotrauma private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; public int StoreInitialBalance => Type.StoreInitialBalance; private int StorePriceModifierRange => Type.StorePriceModifierRange; - private List StoreBalanceStatuses => Type.StoreBalanceStatuses; /// /// How many map progress steps it takes before the discounts should be updated. @@ -518,6 +485,19 @@ namespace Barotrauma TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty); + if (biomeId != Identifier.Empty) + { + if (Biome.Prefabs.TryGet(biomeId, out Biome biome)) + { + Biome = biome; + } + else + { + DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\"."); + } + } + if (!typeNotFound) { for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -806,22 +786,41 @@ namespace Barotrauma static float GetConnectionWeight(Location location, LocationConnection c) { - float weight = c.Passed ? 1.0f : 5.0f; Location destination = c.OtherLocation(location); - if (destination != null) + if (destination == null) { return 0; } + float minWeight = 0.0001f; + float lowWeight = 0.2f; + float normalWeight = 1.0f; + float maxWeight = 2.0f; + float weight = c.Passed ? lowWeight : normalWeight; + if (location.Biome.AllowedZones.Contains(1)) { - if (destination.MapPosition.X > location.MapPosition.X) { weight *= 2.0f; } - int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination)); - if (missionCount > 0) - { - weight /= missionCount * 2; - } - if (destination.IsRadiated()) + // In the first biome, give a stronger preference for locations that are farther to the right) + float diff = destination.MapPosition.X - location.MapPosition.X; + if (diff < 0) { - weight *= 0.001f; + weight *= 0.1f; + } + else + { + float maxRelevantDiff = 300; + weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff)); } } - return weight; + else if (destination.MapPosition.X > location.MapPosition.X) + { + weight *= 2.0f; + } + int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination)); + if (missionCount > 0) + { + weight /= missionCount * 2; + } + if (destination.IsRadiated()) + { + weight *= 0.001f; + } + return MathHelper.Clamp(weight, minWeight, maxWeight); } return InstantiateMission(prefab, connection); @@ -1224,6 +1223,32 @@ namespace Barotrauma } } + public float GetStoreReputationModifier(bool buying) + { + if (buying) + { + if (Reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); + } + } + else + { + if (Reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); + } + } + } + public int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1231,21 +1256,6 @@ namespace Barotrauma return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - public StoreBalanceStatus GetStoreBalanceStatus(int balance) - { - StoreBalanceStatus nextStatus = StoreBalanceStatuses[0]; - for (int i = 1; i < StoreBalanceStatuses.Count; i++) - { - var status = StoreBalanceStatuses[i]; - if (status.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance && - ((float)balance / StoreInitialBalance) < status.PercentageOfInitialBalance) - { - nextStatus = status; - } - } - return nextStatus; - } - public void Discover(bool checkTalents = true) { if (Discovered) { return; } @@ -1277,6 +1287,7 @@ namespace Barotrauma new XAttribute("originaltype", (Type ?? OriginalType).Identifier), new XAttribute("basename", BaseName), new XAttribute("name", Name), + new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index e81e2795d..efa64c5d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -88,27 +88,6 @@ namespace Barotrauma public int DailySpecialsCount { get; } = 1; public int RequestedGoodsCount { get; } = 1; - public List StoreBalanceStatuses { get; } = new List() - { - new StoreBalanceStatus(1.0f, 1.0f, Color.White), - new StoreBalanceStatus(0.5f, 0.75f, Color.Orange), - new StoreBalanceStatus(0.25f, 0.2f, Color.Red) - }; - - public struct StoreBalanceStatus - { - public float PercentageOfInitialBalance { get; } - public float SellPriceModifier { get; } - public Color Color { get; } - - public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color) - { - PercentageOfInitialBalance = percentage; - SellPriceModifier = sellPriceModifier; - Color = color; - } - } - public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -208,18 +187,6 @@ namespace Barotrauma RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier); StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance); StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange); - var balanceStatusElements = subElement.GetChildElements("balancestatus"); - if (balanceStatusElements.Any()) - { - StoreBalanceStatuses.Clear(); - foreach (var balanceStatusElement in balanceStatusElements) - { - float percentage = balanceStatusElement.GetAttributeFloat("percentage", 1.0f); - float modifier = balanceStatusElement.GetAttributeFloat("sellpricemodifier", 1.0f); - Color color = balanceStatusElement.GetAttributeColor("color", Color.White); - StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color)); - } - } DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount); RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 478350b10..274935593 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -78,7 +78,7 @@ namespace Barotrauma /// /// Load a previously saved campaign map from XML /// - private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings) + private Map(CampaignMode campaign, XElement element) : this(campaign.Settings) { Seed = element.GetAttributeString("seed", "a"); Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); @@ -104,7 +104,7 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) { - Enabled = settings.RadiationEnabled + Enabled = campaign.Settings.RadiationEnabled }; break; } @@ -131,18 +131,27 @@ namespace Barotrauma }; Locations[locationIndices.X].Connections.Add(connection); Locations[locationIndices.Y].Connections.Add(connection); - connection.LevelData = new LevelData(subElement.Element("Level")); string biomeId = subElement.GetAttributeString("biome", ""); connection.Biome = Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ?? Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ?? Biome.Prefabs.First(); + connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.MaxDifficulty); + connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty); Connections.Add(connection); connectionElements.Add(subElement); break; } } + //backwards compatibility: location biomes weren't saved (or used for anything) previously, + //assign them if they haven't been assigned + Random rand = new MTRandom(ToolBox.StringToInt(Seed)); + if (Locations.First().Biome == null) + { + AssignBiomes(rand); + } + int startLocationindex = element.GetAttributeInt("startlocation", -1); if (startLocationindex > 0 && startLocationindex < Locations.Count) { @@ -199,12 +208,12 @@ namespace Barotrauma /// /// Generate a new campaign map from the seed /// - public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings) + public Map(CampaignMode campaign, string seed) : this(campaign.Settings) { Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(); + Generate(campaign.Settings); if (Locations.Count == 0) { @@ -219,10 +228,7 @@ namespace Barotrauma foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) - { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - } + SetStartLocation(location); } //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) @@ -230,17 +236,47 @@ namespace Barotrauma foreach (Location location in Locations) { if (!location.Type.HasOutpost) { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + SetStartLocation(location); + } + } + + void SetStartLocation(Location location) + { + if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + { + CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + } + } + + System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); + + int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); + if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium)) + { + if (StartLocation != null) + { + StartLocation.LevelData = new LevelData(StartLocation, 0); + } + + //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy + foreach (var locationConnection in StartLocation.Connections) + { + if (locationConnection.Difficulty > 0.0f) { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + locationConnection.Difficulty = 0.0f; + locationConnection.LevelData = new LevelData(locationConnection); } } } - System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); CurrentLocation.Discover(true); CurrentLocation.CreateStores(); + foreach (var location in Locations) + { + location.UnlockInitialMissions(); + } + InitProjectSpecific(); } @@ -248,7 +284,7 @@ namespace Barotrauma #region Generation - private void Generate() + private void Generate(CampaignSettings settings) { Connections.Clear(); Locations.Clear(); @@ -266,7 +302,6 @@ namespace Barotrauma Voronoi voronoi = new Voronoi(0.5f); List edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height); - float zoneWidth = Width / generationParams.DifficultyZones; Vector2 margin = new Vector2( Math.Min(10, Width * 0.1f), @@ -282,6 +317,7 @@ namespace Barotrauma voronoiSites.Clear(); Dictionary> locationsPerZone = new Dictionary>(); + bool possibleStartOutpostCreated = false; foreach (GraphEdge edge in edges) { if (edge.Point1 == edge.Point2) { continue; } @@ -316,12 +352,26 @@ namespace Barotrauma } LocationType forceLocationType = null; - foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + if (!possibleStartOutpostCreated) { - if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + float zoneWidth = Width / generationParams.DifficultyZones; + float threshold = zoneWidth * 0.1f; + if (position.X < threshold) { - forceLocationType = locationType; - break; + LocationType.Prefabs.TryGet("outpost", out forceLocationType); + possibleStartOutpostCreated = true; + } + } + + if (forceLocationType == null) + { + foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + { + if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + { + forceLocationType = locationType; + break; + } } } @@ -427,9 +477,7 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 > zone2) { - int temp = zone2; - zone2 = zone1; - zone1 = temp; + (zone1, zone2) = (zone2, zone1); } if (generationParams.GateCount[zone1] == 0) { continue; } @@ -495,38 +543,46 @@ namespace Barotrauma //remove orphans Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l))); + AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed))); + foreach (LocationConnection connection in Connections) { - //float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); - //connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.ServerAndClient), 1.2f, 100.0f); - float difficulty = connection.CenterPos.X / Width * 100; - float random = difficulty > 10 ? 5 : 0; - connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f); + if (connection.Locations.Any(l => l.IsGateBetweenBiomes)) + { + connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty); + } + else + { + connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome); + } } - AssignBiomes(); CreateEndLocation(); foreach (Location location in Locations) { - location.LevelData = new LevelData(location) - { - Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) - //Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) - }; - location.UnlockInitialMissions(); + location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); } foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } - float GetLevelDifficulty(float areaDifficulty) + float CalculateDifficulty(float mapPosition, Biome biome) { - const float CurveModifier = 1.5f; - const float DifficultyMultiplier = 1.14f; - const float BaseDifficulty = -3f; - return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty; + float settingsFactor = settings.LevelDifficultyMultiplier; + float minDifficulty = 0; + float maxDifficulty = 100; + float difficulty = mapPosition / Width * 100; + System.Diagnostics.Debug.Assert(biome != null); + if (biome != null) + { + minDifficulty = biome.MinDifficulty; + maxDifficulty = biome.MaxDifficulty; + float diff = 1 - settingsFactor; + difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff); + } + return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); } } @@ -551,7 +607,7 @@ namespace Barotrauma return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); } - private void AssignBiomes() + private void AssignBiomes(Random rand) { var biomes = Biome.Prefabs; float zoneWidth = Width / generationParams.DifficultyZones; @@ -567,7 +623,7 @@ namespace Barotrauma { if (location.MapPosition.X < zoneX) { - location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)]; + location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count]; } } } @@ -608,6 +664,11 @@ namespace Barotrauma if (EndLocation == null || previousToEndLocation == null) { return; } + if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) + { + previousToEndLocation.ChangeType(locationType); + } + //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { @@ -627,7 +688,7 @@ namespace Barotrauma } //removed all connections from the second-to-last location, need to reconnect it - if (!previousToEndLocation.Connections.Any()) + if (previousToEndLocation.Connections.None()) { Location connectTo = Locations.First(); foreach (Location location in Locations) @@ -734,6 +795,7 @@ namespace Barotrauma CurrentLocation = Locations[index]; CurrentLocation.Discover(); + CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) { var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation)); @@ -741,10 +803,8 @@ namespace Barotrauma { connection.Passed = true; } + OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } - - CurrentLocation.CreateStores(); - OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } public void SelectLocation(int index) @@ -764,6 +824,7 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); SelectedConnection = @@ -773,7 +834,10 @@ namespace Barotrauma { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectLocation(Location location) @@ -786,13 +850,17 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = location; SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); if (SelectedConnection?.Locked ?? false) { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectMission(IEnumerable missionIndices) @@ -805,23 +873,24 @@ namespace Barotrauma return; } - CurrentLocation.SetSelectedMissionIndices(missionIndices); - - foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) + if (!missionIndices.SequenceEqual(GetSelectedMissionIndices())) { - if (selectedMission.Locations[0] != CurrentLocation || - selectedMission.Locations[1] != CurrentLocation) + CurrentLocation.SetSelectedMissionIndices(missionIndices); + foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) { - if (SelectedConnection == null) { return; } - //the destination must be the same as the destination of the mission - if (selectedMission.Locations[1] != SelectedLocation) + if (selectedMission.Locations[0] != CurrentLocation || + selectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.DeselectMission(selectedMission); + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (selectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.DeselectMission(selectedMission); + } } } + OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } - - OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } public void SelectRandomLocation(bool preferUndiscovered) @@ -1015,8 +1084,7 @@ namespace Barotrauma { string prevName = location.Name; - var newType = LocationType.Prefabs[change.ChangeToType]; - if (newType == null) + if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType)) { DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); return false; @@ -1046,9 +1114,9 @@ namespace Barotrauma /// /// Load a previously saved map from an xml element /// - public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings) + public static Map Load(CampaignMode campaign, XElement element) { - Map map = new Map(campaign, element, settings); + Map map = new Map(campaign, element); map.LoadState(element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index fb989ec73..42ca533d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -291,7 +291,7 @@ namespace Barotrauma } } - public virtual void Move(Vector2 amount) + public virtual void Move(Vector2 amount, bool ignoreContacts = false) { rect.X += (int)amount.X; rect.Y += (int)amount.Y; @@ -491,25 +491,33 @@ namespace Barotrauma protected void InsertToList() { - int i = 0; - if (Sprite == null) { mapEntityList.Add(this); return; } + int i = 0; while (i < mapEntityList.Count) { i++; - - Sprite existingSprite = mapEntityList[i - 1].Sprite; - if (existingSprite == null) continue; -#if CLIENT - if (existingSprite.Texture == this.Sprite.Texture) break; -#endif + if (mapEntityList[i - 1]?.Prefab == Prefab) + { + mapEntityList.Insert(i, this); + return; + } } +#if CLIENT + i = 0; + while (i < mapEntityList.Count) + { + i++; + Sprite existingSprite = mapEntityList[i - 1].Sprite; + if (existingSprite == null) { continue; } + if (existingSprite.Texture == this.Sprite.Texture) { break; } + } +#endif mapEntityList.Insert(i, this); } @@ -559,6 +567,10 @@ namespace Barotrauma /// public static void UpdateAll(float deltaTime, Camera cam) { +#if CLIENT + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); +#endif foreach (Hull hull in Hull.HullList) { hull.Update(deltaTime, cam); @@ -586,6 +598,11 @@ namespace Barotrauma gapUpdateTimer = 0; } +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); + sw.Restart(); +#endif Powered.UpdatePower(deltaTime); foreach (Item item in Item.ItemList) { @@ -594,6 +611,11 @@ namespace Barotrauma UpdateAllProjSpecific(deltaTime); +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks); + sw.Restart(); +#endif Spawner?.Update(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs new file mode 100644 index 000000000..7d9f33be6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + class BeaconStationInfo : ISerializableEntity + { + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedWalls { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDisconnectedWires { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float MinLevelDifficulty { get; set; } + + [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + public float MaxLevelDifficulty { get; set; } + + public string Name { get; private set; } + + public Dictionary SerializableProperties { get; private set; } + + public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public BeaconStationInfo(SubmarineInfo submarineInfo) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this); + } + + public BeaconStationInfo(BeaconStationInfo original) + { + Name = original.Name; + SerializableProperties = new Dictionary(); + foreach (KeyValuePair kvp in original.SerializableProperties) + { + SerializableProperties.Add(kvp.Key, kvp.Value); + if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null) + { + kvp.Value.TrySetValue(this, kvp.Value.GetValue(original)); + } + } + } + + public void Save(XElement element) + { + SerializableProperty.SerializeProperties(this, element); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 5397af405..48cce8bfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -34,23 +34,6 @@ namespace Barotrauma return prefab; } - private void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - Humans.Clear(); - } - } - - Disposed = true; - } - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 6d75e6916..4c65efaa3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -1,11 +1,9 @@ using Barotrauma.Extensions; -using Microsoft.Xna.Framework; using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -98,9 +96,29 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } - private readonly Dictionary moduleCounts = new Dictionary(); + public class ModuleCount + { + public Identifier Identifier; + public int Count; + public int Order; - public IReadOnlyDictionary ModuleCounts + public ModuleCount(ContentXElement element) + { + Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", "")); + Count = element.GetAttributeInt("count", 0); + Order = element.GetAttributeInt("order", 0); + } + + public ModuleCount(Identifier id, int count) + { + Identifier = id; + Count = count; + } + } + + private readonly List moduleCounts = new List(); + + public IReadOnlyList ModuleCounts { get { return moduleCounts; } } @@ -171,8 +189,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "modulecount": - Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", "")); - moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0); + moduleCounts.Add(new ModuleCount(subElement)); break; case "npcs": var newCollection = new NpcCollection(); @@ -200,7 +217,7 @@ namespace Barotrauma public int GetModuleCount(Identifier moduleFlag) { if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; } - return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0; + return moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag)?.Count ?? 0; } public void SetModuleCount(Identifier moduleFlag, int count) @@ -208,11 +225,19 @@ namespace Barotrauma if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; } if (count <= 0) { - moduleCounts.Remove(moduleFlag); + moduleCounts.RemoveAll(m => m.Identifier == moduleFlag); } else { - moduleCounts[moduleFlag] = count; + var moduleCount = moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag); + if (moduleCount == null) + { + moduleCounts.Add(new ModuleCount(moduleFlag, count)); + } + else + { + moduleCount.Count = count; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 295b92751..a0e5ceed6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -99,7 +99,7 @@ namespace Barotrauma { //if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) && - !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key))) + !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Identifier))) { continue; } @@ -141,16 +141,11 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags; - using (var md5 = MD5.Create()) - { - #warning TODO: cursed - pendingModuleFlags = onlyEntrance - ? generationParams.ModuleCounts - .Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5)) - .First().ToEnumerable().ToList() - : SelectModules(outpostModules, generationParams); - } + List pendingModuleFlags = + onlyEntrance ? + generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, generationParams); + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -437,31 +432,27 @@ namespace Barotrauma var pendingModuleFlags = new List(); bool availableModulesFound = true; - Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; + Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier; pendingModuleFlags.Add(initialModuleFlag); while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; foreach (var moduleFlag in generationParams.ModuleCounts) { - if (pendingModuleFlags.Count(m => m == moduleFlag.Key) >= generationParams.GetModuleCount(moduleFlag.Key)) + if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier)) { continue; } - if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Key))) + if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier))) { - DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Key}\" found)."); + DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found)."); continue; } availableModulesFound = true; - pendingModuleFlags.Add(moduleFlag.Key); + pendingModuleFlags.Add(moduleFlag.Identifier); } } - using (MD5 md5 = MD5.Create()) - { - pendingModuleFlags.Sort((i1, i2) => (int)ToolBox.StringToUInt32Hash(i1.Value.ToLowerInvariant(), md5) - (int)ToolBox.StringToUInt32Hash(i2.Value.ToLowerInvariant(), md5)); - } - pendingModuleFlags.Shuffle(Rand.RandSync.ServerAndClient); + pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); while (pendingModuleFlags.Count < totalModuleCount) { //don't place "none" modules at the end because @@ -610,7 +601,7 @@ namespace Barotrauma Identifier flagToPlace = "none".ToIdentifier(); SubmarineInfo nextModule = null; - foreach (Identifier moduleFlag in pendingModuleFlags) + foreach (Identifier moduleFlag in pendingModuleFlags.OrderByDescending(f => currentModule?.Info?.OutpostModuleInfo.AllowAttachToModules.Contains(f) ?? false)) { flagToPlace = moduleFlag; nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); @@ -830,43 +821,44 @@ namespace Barotrauma private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { - IEnumerable availableModules = null; + IEnumerable modulesWithCorrectFlags = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - - if (prevModule != null) + var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + if (!suitableModules.Any()) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); + //no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + //still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + } + //still not found! Try if we can find a generic module that's not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + } } - if (availableModules.Count() == 0) { return null; } - - //try to search for modules made specifically for this location type first - var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); - - //if not found, search for modules suitable for any location type - if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) - { - modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); - } - - if (!modulesSuitableForLocationType.Any()) + if (!suitableModules.Any()) { if (allowDifferentLocationType) { + if (modulesWithCorrectFlags.Any()) + DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { @@ -875,7 +867,28 @@ namespace Barotrauma } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + } + + IEnumerable GetSuitable(IEnumerable modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific) + { + IEnumerable suitable = modules; + if (requireCorrectLocationType) + { + if (disallowNonLocationTypeSpecific) + { + suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); + } + else + { + suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } + } + if (requireAllowAttachToPrevious && prevModule != null) + { + suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule)); + } + return suitable; } } @@ -1026,6 +1039,17 @@ namespace Barotrauma module.ThisGapPosition == OutpostModuleInfo.GapPosition.Left || module.ThisGapPosition == OutpostModuleInfo.GapPosition.Right; + if (!module.ThisGap.linkedTo.Any()) + { + DebugConsole.ThrowError($"Error during outpost generation: {module.ThisGapPosition} gap in module \"{module.Info.Name}\" was not linked to any hulls."); + continue; + } + if (!module.PreviousGap.linkedTo.Any()) + { + DebugConsole.ThrowError($"Error during outpost generation: {GetOpposingGapPosition(module.ThisGapPosition)} gap in module \"{module.PreviousModule.Info.Name}\" was not linked to any hulls."); + continue; + } + MapEntity leftHull = module.ThisGap.Position.X < module.PreviousGap.Position.X ? module.ThisGap.linkedTo[0] : module.PreviousGap.linkedTo[0]; MapEntity rightHull = module.ThisGap.Position.X > module.PreviousGap.Position.X ? module.ThisGap.linkedTo.Count == 1 ? module.ThisGap.linkedTo[0] : module.ThisGap.linkedTo[1] : @@ -1077,7 +1101,7 @@ namespace Barotrauma { foreach (Connection c in gapToRemove.ConnectedDoor.Item.Connections) { - c.Wires.ForEach(w => w?.Item.Remove()); + c.Wires.ToArray().ForEach(w => w?.Item.Remove()); } } @@ -1428,7 +1452,7 @@ namespace Barotrauma { foreach (Connection connection in linkedItem.Connections) { - foreach (Wire w in connection.Wires) + foreach (Wire w in connection.Wires.ToArray()) { w?.Item.Remove(); } @@ -1590,10 +1614,6 @@ namespace Barotrauma { npc.CharacterHealth.Unkillable = true; } - else - { - npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); - } humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index d3901b682..b973831de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -354,7 +354,7 @@ namespace Barotrauma private set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -377,7 +377,15 @@ namespace Barotrauma Vector2 simAmount = ConvertUnits.ToSimUnits(amount); foreach (Body b in Bodies) { - b.SetTransform(b.Position + simAmount, b.Rotation); + Vector2 pos = b.Position + simAmount; + if (ignoreContacts) + { + b.SetTransformIgnoreContacts(ref pos, b.Rotation); + } + else + { + b.SetTransform(pos, b.Rotation); + } } } @@ -1208,7 +1216,7 @@ namespace Barotrauma private void UpdateSections() { - if (Bodies == null) return; + if (Bodies == null) { return; } foreach (Body b in Bodies) { GameMain.World.Remove(b); @@ -1281,9 +1289,9 @@ namespace Barotrauma Body newBody = GameMain.World.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), - 1.5f); - newBody.BodyType = BodyType.Static; - //newBody.Position = ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2.0f, rect.Y - rect.Height / 2.0f)); + 1.5f, + bodyType: BodyType.Static, + findNewContacts: false); newBody.Friction = 0.5f; newBody.OnCollision += OnWallCollision; newBody.CollisionCategories = (Prefab.Platform) ? Physics.CollisionPlatform : Physics.CollisionWall; @@ -1292,15 +1300,16 @@ namespace Barotrauma Vector2 structureCenter = ConvertUnits.ToSimUnits(Position); if (BodyRotation != 0.0f) { - newBody.Position = structureCenter + bodyOffset + new Vector2( + Vector2 pos = structureCenter + bodyOffset + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * ConvertUnits.ToSimUnits(diffFromCenter); - newBody.Rotation = -BodyRotation; + newBody.SetTransformIgnoreContacts(ref pos, -BodyRotation); } else { - newBody.Position = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset; + Vector2 pos = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset; + newBody.SetTransformIgnoreContacts(ref pos, newBody.Rotation); } if (createConvexHull) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index a046b1098..dc47f42f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1302,189 +1302,200 @@ namespace Barotrauma public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { Loading = true; - - loaded.Add(this); - - Info = new SubmarineInfo(info); - - ConnectedDockingPorts = new Dictionary(); - - //place the sub above the top of the level - HiddenSubPosition = HiddenSubStartPosition; - if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + GameMain.World.Enabled = false; + try { - HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; - } + loaded.Add(this); - foreach (Submarine sub in loaded) - { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); - } + Info = new SubmarineInfo(info); - IdOffset = IdRemap.DetermineNewOffset(); + ConnectedDockingPorts = new Dictionary(); - List newEntities = new List(); - if (loadEntities == null) - { - if (Info.SubmarineElement != null) + //place the sub above the top of the level + HiddenSubPosition = HiddenSubStartPosition; + if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) { - newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset); - } - } - else - { - newEntities = loadEntities(this); - newEntities.ForEach(me => me.Submarine = this); - } - - if (newEntities != null) - { - foreach (var e in newEntities) - { - if (linkedRemap != null) { e.ResolveLinks(linkedRemap); } - e.unresolvedLinkedToID = null; - } - } - - Vector2 center = Vector2.Zero; - var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); - - if (matchingHulls.Any()) - { - Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); - Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); - foreach (Hull hull in matchingHulls) - { - if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; - if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; - - if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; - if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; + HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - center = (topLeft + bottomRight) / 2.0f; - center.X -= center.X % GridSize.X; - center.Y -= center.Y % GridSize.Y; - - RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); - - subBody = new SubmarineBody(this, showWarningMessages); - subBody.SetPosition(HiddenSubPosition); - - if (info.IsOutpost) + foreach (Submarine sub in loaded) { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = CharacterTeamType.FriendlyNPC; + HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + } - bool indestructible = - GameMain.NetworkMember != null && - !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && - !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + IdOffset = IdRemap.DetermineNewOffset(); - foreach (MapEntity me in MapEntity.mapEntityList) + List newEntities = new List(); + if (loadEntities == null) + { + if (Info.SubmarineElement != null) { - if (me.Submarine != this) { continue; } - if (me is Item item) + newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset); + } + } + else + { + newEntities = loadEntities(this); + newEntities.ForEach(me => me.Submarine = this); + } + + if (newEntities != null) + { + foreach (var e in newEntities) + { + if (linkedRemap != null) { e.ResolveLinks(linkedRemap); } + e.unresolvedLinkedToID = null; + } + } + + Vector2 center = Vector2.Zero; + var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); + + if (matchingHulls.Any()) + { + Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); + Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); + foreach (Hull hull in matchingHulls) + { + if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; + if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; + + if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; + if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; + } + + center = (topLeft + bottomRight) / 2.0f; + center.X -= center.X % GridSize.X; + center.Y -= center.Y % GridSize.Y; + + RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + + subBody = new SubmarineBody(this, showWarningMessages); + Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); + subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); + + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = CharacterTeamType.FriendlyNPC; + + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + + foreach (MapEntity me in MapEntity.mapEntityList) { - item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; - item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; - if (item.GetComponent() != null && indestructible) + if (me.Submarine != this) { continue; } + if (me is Item item) { - item.Indestructible = true; - } - foreach (ItemComponent ic in item.Components) - { - if (ic is ConnectionPanel connectionPanel) + item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; + item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + if (item.GetComponent() != null && indestructible) { - //prevent rewiring - if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) { - connectionPanel.Locked = true; + //prevent rewiring + if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) + { + connectionPanel.Locked = true; + } + } + else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) + { + //prevent deattaching items from walls +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } +#endif + holdable.CanBePicked = false; + holdable.CanBeSelected = false; } } - else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) - { - //prevent deattaching items from walls -#if CLIENT - if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } -#endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } + } + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) + { + structure.Indestructible = true; } } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) + } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + } + } + + if (entityGrid != null) + { + Hull.EntityGrids.Remove(entityGrid); + entityGrid = null; + } + entityGrid = Hull.GenerateEntityGrid(this); + + for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + { + if (MapEntity.mapEntityList[i].Submarine != this) { continue; } + MapEntity.mapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); + } + + Loading = false; + + MapEntity.MapLoaded(newEntities, true); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + linkedSub.LinkDummyToMainSubmarine(); + } + } + + foreach (Hull hull in matchingHulls) + { + if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) + { + hull.RoomName = hull.CreateRoomName(); + } + } + + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) + { + GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth; + } + +#if CLIENT + GameMain.LightManager.OnMapLoaded(); +#endif + //if the sub was made using an older version, + //halve the brightness of the lights to make them look (almost) right on the new lighting formula + if (showWarningMessages && + !string.IsNullOrEmpty(Info.FilePath) && + Screen.Selected != GameMain.SubEditorScreen && + (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) + { + DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); + foreach (Item item in Item.ItemList) + { + if (item.Submarine != this) continue; + if (item.ParentInventory != null || item.body != null) continue; + foreach (var light in item.GetComponents()) { - structure.Indestructible = true; + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); } } } - else if (info.IsRuin) - { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - } + GenerateOutdoorNodes(); } - - if (entityGrid != null) + finally { - Hull.EntityGrids.Remove(entityGrid); - entityGrid = null; + Loading = false; + GameMain.World.Enabled = true; } - entityGrid = Hull.GenerateEntityGrid(this); - - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) - { - if (MapEntity.mapEntityList[i].Submarine != this) { continue; } - MapEntity.mapEntityList[i].Move(HiddenSubPosition); - } - - Loading = false; - - MapEntity.MapLoaded(newEntities, true); - foreach (MapEntity me in MapEntity.mapEntityList) - { - if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) - { - linkedSub.LinkDummyToMainSubmarine(); - } - } - - foreach (Hull hull in matchingHulls) - { - if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) - { - hull.RoomName = hull.CreateRoomName(); - } - } - - if (GameMain.GameSession?.Campaign?.UpgradeManager != null) - { - GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth; - } - -#if CLIENT - GameMain.LightManager.OnMapLoaded(); -#endif - //if the sub was made using an older version, - //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && - !string.IsNullOrEmpty(Info.FilePath) && - Screen.Selected != GameMain.SubEditorScreen && - (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) - { - DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " - + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); - foreach (Item item in Item.ItemList) - { - if (item.Submarine != this) continue; - if (item.ParentInventory != null || item.body != null) continue; - var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); - } - } - GenerateOutdoorNodes(); } protected override ushort DetermineID(ushort id, Submarine submarine) @@ -1495,10 +1506,7 @@ namespace Barotrauma public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null) { if (unloadPrevious) { Unload(); } - - Submarine sub = new Submarine(info, false, linkedRemap: linkedRemap); - - return sub; + return new Submarine(info, false, linkedRemap: linkedRemap); } private void ResetCrushDepth() @@ -1549,7 +1557,7 @@ namespace Barotrauma element.Add(new XAttribute("cargocapacity", cargoCapacity)); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); - element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); + element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString())); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); if (Info.Type == SubmarineType.OutpostModule) @@ -1599,18 +1607,14 @@ namespace Barotrauma { if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } #if CLIENT - if (Screen.Selected != GameMain.SubEditorScreen) - { - if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } - } - else + if (Screen.Selected == GameMain.SubEditorScreen) { e.Submarine = this; } -#else - if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } #endif - + if (e.Submarine != this) { continue; } + var rootContainer = item.GetRootContainer(); + if (rootContainer != null && rootContainer.Submarine != this) { continue; } } else { @@ -1630,6 +1634,7 @@ namespace Barotrauma Type = Info.Type, FilePath = filePath, OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, + BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; #if CLIENT @@ -1851,5 +1856,42 @@ namespace Barotrauma } public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull()); + + public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false, bool allowConnectedSubs = false) + { + var connectedSubs = GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + Item selectedContainer = null; + foreach (Item potentialContainer in Item.ItemList) + { + if (potentialContainer.Removed) { continue; } + if (potentialContainer.NonInteractable) { continue; } + if (potentialContainer.HiddenInGame) { continue; } + if (allowConnectedSubs) + { + if (!connectedSubs.Contains(potentialContainer.Submarine)) { continue; } + } + else + { + if (potentialContainer.Submarine != this) { continue; } + } + if (potentialContainer == item) { continue; } + if (potentialContainer.Condition <= 0) { continue; } + if (potentialContainer.OwnInventory == null) { continue; } + if (potentialContainer.GetRootInventoryOwner() != potentialContainer) { continue; } + var container = potentialContainer.GetComponent(); + if (container == null) { continue; } + if (!potentialContainer.OwnInventory.CanBePut(item)) { continue; } + if (!container.ShouldBeContained(item, out _)) { continue; } + if (!item.Prefab.IsContainerPreferred(item, container, out bool isPreferencesDefined, out bool isSecondary, checkTransferConditions: checkTransferConditions) || !isPreferencesDefined || onlyPrimary && isSecondary) { continue; } + if (potentialContainer.Submarine == this && !isSecondary) + { + //valid primary container in the same sub -> perfect, let's use that one + return potentialContainer; + } + selectedContainer = potentialContainer; + + } + return selectedContainer; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 3216589a1..c2340805b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -136,7 +136,17 @@ namespace Barotrauma HullVertices = convexHull; - farseerBody = GameMain.World.CreateBody(); + farseerBody = GameMain.World.CreateBody(findNewContacts: false, bodyType: BodyType.Dynamic); + var collisionCategory = Physics.CollisionWall; + var collidesWith = + Physics.CollisionItem | + Physics.CollisionLevel | + Physics.CollisionCharacter | + Physics.CollisionProjectile | + Physics.CollisionWall; + farseerBody.CollisionCategories = collisionCategory; + farseerBody.CollidesWith = collidesWith; + farseerBody.Enabled = false; farseerBody.UserData = this; foreach (var mapEntity in MapEntity.mapEntityList) { @@ -152,7 +162,9 @@ namespace Barotrauma ConvertUnits.ToSimUnits(wall.BodyHeight), 50.0f, -wall.BodyRotation, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset), + collisionCategory, + collidesWith).UserData = wall; } } @@ -167,7 +179,9 @@ namespace Barotrauma ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), 100.0f, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull; + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2)), + collisionCategory, + collidesWith).UserData = hull; } foreach (Item item in Item.ItemList) @@ -191,47 +205,40 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith)); SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true); } else if (radius > 0.0f && width > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); } else if (radius > 0.0f && height > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); + item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); } else if (radius > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith)); visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true); } + item.StaticFixtures.ForEach(f => f.UserData = item); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); VisibleBorders = new Rectangle((int)visibleMinExtents.X, (int)visibleMaxExtents.Y, (int)(visibleMaxExtents.X - visibleMinExtents.X), (int)(visibleMaxExtents.Y - visibleMinExtents.Y)); } - farseerBody.BodyType = BodyType.Dynamic; - farseerBody.CollisionCategories = Physics.CollisionWall; - farseerBody.CollidesWith = - Physics.CollisionItem | - Physics.CollisionLevel | - Physics.CollisionCharacter | - Physics.CollisionProjectile | - Physics.CollisionWall; - + farseerBody.Enabled = true; farseerBody.Restitution = Restitution; farseerBody.Friction = Friction; farseerBody.FixedRotation = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index fb29d2652..1e37ab37e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -39,7 +39,15 @@ namespace Barotrauma public SubmarineTag Tags { get; private set; } public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; - public string RecommendedCrewExperience; + + public enum CrewExperienceLevel + { + Unknown, + CrewExperienceLow, + CrewExperienceMid, + CrewExperienceHigh + } + public CrewExperienceLevel RecommendedCrewExperience; /// /// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match @@ -89,6 +97,7 @@ namespace Barotrauma public SubmarineClass SubmarineClass; public OutpostModuleInfo OutpostModuleInfo { get; set; } + public BeaconStationInfo BeaconStationInfo { get; set; } public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; @@ -100,6 +109,8 @@ namespace Barotrauma public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined; public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus); + public bool AllowPreviewImage => Type == SubmarineType.Player; + public Md5Hash MD5Hash { get @@ -280,6 +291,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } + if (original.BeaconStationInfo != null) + { + BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo); + } #if CLIENT PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif @@ -330,7 +345,24 @@ namespace Barotrauma CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1); RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); - RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); + var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier()); + // Backwards compatibility + if (recommendedCrewExperience == "Beginner") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow; + } + else if (recommendedCrewExperience == "Intermediate") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid; + } + else if (recommendedCrewExperience == "Experienced") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh; + } + else + { + Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience); + } if (SubmarineElement?.Attribute("type") != null) { @@ -341,6 +373,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement); + } } } @@ -359,20 +395,6 @@ namespace Barotrauma SubmarineClass = SubmarineClass.Undefined; } - //backwards compatibility (use text tags instead of the actual text) - if (RecommendedCrewExperience == "Beginner") - { - RecommendedCrewExperience = "CrewExperienceLow"; - } - else if (RecommendedCrewExperience == "Intermediate") - { - RecommendedCrewExperience = "CrewExperienceMid"; - } - else if (RecommendedCrewExperience == "Experienced") - { - RecommendedCrewExperience = "CrewExperienceHigh"; - } - RequiredContentPackages.Clear(); string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty()); foreach (string contentPackageName in contentPackageNames) @@ -528,10 +550,15 @@ namespace Barotrauma OutpostModuleInfo.Save(newElement); OutpostModuleInfo = new OutpostModuleInfo(this, newElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo.Save(newElement); + BeaconStationInfo = new BeaconStationInfo(this, newElement); + } XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); - if (previewImage != null) + if (previewImage != null && AllowPreviewImage) { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); } @@ -590,6 +617,7 @@ namespace Barotrauma List filePaths = new List(); foreach (BaseSubFile subFile in contentPackageSubs) { + if (!File.Exists(subFile.Path.Value)) { continue; } if (!filePaths.Any(fp => fp == subFile.Path)) { filePaths.Add(subFile.Path.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 23a7d0e1f..6972ac705 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -109,14 +109,21 @@ namespace Barotrauma #endif } - + public enum Type + { + WayPoint, + SpawnPoint + } + public WayPoint(Rectangle newRect, Submarine submarine) - : this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine) + : this (Type.WayPoint, newRect, submarine) { } - public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID) - : base (prefab, submarine, id) + public WayPoint(Type type, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID) + : base (type is Type.WayPoint + ? CoreEntityPrefab.WayPointPrefab + : CoreEntityPrefab.SpawnPointPrefab, submarine, id) { rect = newRect; idCardTags = Array.Empty(); @@ -1010,7 +1017,7 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); - WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element)) + WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { spawnType = spawnType }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 3706116ef..64e8c5517 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -117,6 +117,8 @@ namespace Barotrauma.Networking set; } + public ChatMode ChatMode { get; set; } = ChatMode.None; + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { Text = text; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 4defc4944..144a567ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -107,10 +107,25 @@ namespace Barotrauma.Networking return -1; } + // BUG workaround for crash when closing the server under .NET 6.0, not sure if this is the proper way to fix it but it prevents it from crashing the client. - Markus +#if NET6_0 + try + { + if (readTask.IsCompleted || readTask.Wait(100, readCancellationToken.Token)) + { + break; + } + } + catch (OperationCanceledException) + { + return -1; + } +#else if (readTask.IsCompleted || readTask.Wait(timeOut)) { break; } +#endif } if (readTask.Status != TaskStatus.RanToCompletion) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index b8c2d6419..afa43e536 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -28,6 +28,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization + UPDATE_CHARACTERINFO, + ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 50a94dbe5..b80fd2455 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -21,6 +21,8 @@ namespace Barotrauma.Networking private readonly NetworkMember networkMember; private readonly Steering shuttleSteering; private readonly List shuttleDoors; + private const string RespawnContainerTag = "respawncontainer"; + private readonly ItemContainer respawnContainer; //items created during respawn //any respawn items left in the shuttle are removed when the shuttle despawns @@ -100,13 +102,18 @@ namespace Barotrauma.Networking shuttleDoors = new List(); foreach (Item item in Item.ItemList) { - if (item.Submarine != RespawnShuttle) continue; + if (item.Submarine != RespawnShuttle) { continue; } + + if (item.HasTag(RespawnContainerTag)) + { + respawnContainer = item.GetComponent(); + } var steering = item.GetComponent(); - if (steering != null) shuttleSteering = steering; + if (steering != null) { shuttleSteering = steering; } var door = item.GetComponent(); - if (door != null) shuttleDoors.Add(door); + if (door != null) { shuttleDoors.Add(door); } //lock all wires to prevent the players from messing up the electronics var connectionPanel = item.GetComponent(); @@ -227,14 +234,14 @@ namespace Barotrauma.Networking despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30); #endif - if (RespawnShuttle == null) return; + if (RespawnShuttle == null) { return; } foreach (Item item in Item.ItemList) { if (item.Submarine != RespawnShuttle) { continue; } //remove respawn items that have been left in the shuttle - if (respawnItems.Contains(item)) + if (respawnItems.Contains(item) || respawnContainer?.Item != null && item.IsOwnedBy(respawnContainer.Item)) { Spawner.AddItemToRemoveQueue(item); continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 736099f64..7c612b9ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -39,6 +39,12 @@ namespace Barotrauma.Networking SomethingDifferent = 4 } + internal enum LootedMoneyDestination + { + Bank, + Wallet + } + partial class ServerSettings : ISerializableEntity { public const string SettingsFile = "serversettings.xml"; @@ -894,21 +900,13 @@ namespace Barotrauma.Networking private set; } - [Serialize(true, IsPropertySaveable.Yes)] - public bool RadiationEnabled - { - get; - set; - } + [Serialize(LootedMoneyDestination.Bank, IsPropertySaveable.Yes)] + public LootedMoneyDestination LootedMoneyDestination { get; set; } - private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; + [Serialize(999999, IsPropertySaveable.Yes)] + public int MaximumMoneyTransferRequest { get; set; } - [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } - } + public CampaignSettings CampaignSettings { get; set; } = CampaignSettings.Empty; private bool allowSubVoting; //Don't serialize: the value is set based on SubSelectionMode diff --git a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index bf0a96dd4..f72637960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -23,7 +23,6 @@ namespace Barotrauma private readonly Dictionary> elapsedTicks = new Dictionary>(); private readonly Dictionary avgTicksPerFrame = new Dictionary(); - private readonly Dictionary> partialTickInfos = new Dictionary>(); #if CLIENT internal Graph UpdateTimeGraph = new Graph(500), DrawTimeGraph = new Graph(500); @@ -43,20 +42,6 @@ namespace Barotrauma } } - private readonly List tempSavedPartialIdentifiers = new List(); - public IReadOnlyList GetSavedPartialIdentifiers(string parentIdentifier) - { - lock (mutex) - { - tempSavedPartialIdentifiers.Clear(); - if (partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) - { - tempSavedPartialIdentifiers.AddRange(tickInfos.Keys); - } - } - return tempSavedPartialIdentifiers; - } - public void AddElapsedTicks(string identifier, long ticks) { lock (mutex) @@ -72,29 +57,6 @@ namespace Barotrauma } } - public void AddPartialElapsedTicks(string parentIdentifier, string identifier, long ticks) - { - lock (mutex) - { - if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) - { - tickInfos = new Dictionary(); - partialTickInfos.Add(parentIdentifier, tickInfos); - } - if (!tickInfos.TryGetValue(identifier, out var tickInfo)) - { - tickInfo = new TickInfo(); - tickInfos.Add(identifier, tickInfo); - } - tickInfo.ElapsedTicks.Enqueue(ticks); - if (tickInfo.ElapsedTicks.Count > MaximumSamples) - { - tickInfo.ElapsedTicks.Dequeue(); - tickInfo.AvgTicksPerFrame = (long)tickInfo.ElapsedTicks.Average(i => i); - } - } - } - public float GetAverageElapsedMillisecs(string identifier) { long ticksPerFrame = 0; @@ -105,18 +67,6 @@ namespace Barotrauma return ticksPerFrame * 1000.0f / Stopwatch.Frequency; } - public float GetPartialAverageElapsedMillisecs(string parentIdentifier, string identifier) - { - long ticksPerFrame = 0; - lock (mutex) - { - if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) { return 0.0f; } - if (!tickInfos.TryGetValue(identifier, out var tickInfo)) { return 0.0f; } - ticksPerFrame = tickInfo.AvgTicksPerFrame; - } - return ticksPerFrame * 1000.0f / Stopwatch.Frequency; - } - public bool Update(double deltaTime) { if (deltaTime == 0.0f) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index ae1f8adf7..74da0ce7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -344,14 +344,14 @@ namespace Barotrauma } } - public PhysicsBody(XElement element, float scale = 1.0f) : this(element, Vector2.Zero, scale) { } - public PhysicsBody(ColliderParams cParams) : this(cParams, Vector2.Zero) { } - public PhysicsBody(LimbParams lParams) : this(lParams, Vector2.Zero) { } + public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { } + public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { } + public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { } - public PhysicsBody(float width, float height, float radius, float density) + public PhysicsBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true) { density = Math.Max(density, MinDensity); - CreateBody(width, height, radius, density); + CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); LastSentPosition = FarseerBody.Position; list.Add(this); } @@ -359,21 +359,21 @@ namespace Barotrauma public PhysicsBody(Body farseerBody) { FarseerBody = farseerBody; - if (FarseerBody.UserData == null) FarseerBody.UserData = this; + if (FarseerBody.UserData == null) { FarseerBody.UserData = this; } LastSentPosition = FarseerBody.Position; list.Add(this); } - public PhysicsBody(ColliderParams colliderParams, Vector2 position) + public PhysicsBody(ColliderParams colliderParams, Vector2 position, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = 10; - CreateBody(width, height, radius, density); - FarseerBody.BodyType = BodyType.Dynamic; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - FarseerBody.CollisionCategories = Physics.CollisionCharacter; + CreateBody(width, height, radius, density, BodyType.Dynamic, + Physics.CollisionCharacter, + Physics.CollisionWall | Physics.CollisionLevel, + findNewContacts); FarseerBody.AngularDamping = DefaultAngularDamping; FarseerBody.FixedRotation = true; FarseerBody.Friction = 0.05f; @@ -383,16 +383,24 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(LimbParams limbParams, Vector2 position) + public PhysicsBody(LimbParams limbParams, Vector2 position, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale; density = Math.Max(limbParams.Density, MinDensity); - CreateBody(width, height, radius, density); - FarseerBody.BodyType = BodyType.Dynamic; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - FarseerBody.CollisionCategories = Physics.CollisionItem; + + Category collisionCategory = Physics.CollisionCharacter; + Category collidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; + if (limbParams.IgnoreCollisions) + { + collisionCategory = Category.None; + collidesWith = Category.None; + } + CreateBody(width, height, radius, density, BodyType.Dynamic, + collisionCategory: collisionCategory, + collidesWith: collidesWith, + findNewContacts: findNewContacts); FarseerBody.Friction = limbParams.Friction; FarseerBody.Restitution = limbParams.Restitution; FarseerBody.AngularDamping = limbParams.AngularDamping; @@ -402,17 +410,14 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null) + public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null, Category collisionCategory = Physics.CollisionItem, Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", 10.0f), MinDensity); - CreateBody(width, height, radius, density); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - FarseerBody.BodyType = bodyType; - FarseerBody.CollisionCategories = Physics.CollisionItem; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; + CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; @@ -421,7 +426,7 @@ namespace Barotrauma list.Add(this); } - private void CreateBody(float width, float height, float radius, float density) + private void CreateBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true) { if (IsValidShape(radius, height, width)) { @@ -429,16 +434,16 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - FarseerBody = GameMain.World.CreateCapsule(height, radius, density); + FarseerBody = GameMain.World.CreateCapsule(height, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); ; break; case Shape.HorizontalCapsule: - FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density); + FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; case Shape.Circle: - FarseerBody = GameMain.World.CreateCircle(radius, density); + FarseerBody = GameMain.World.CreateCircle(radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; case Shape.Rectangle: - FarseerBody = GameMain.World.CreateRectangle(width, height, density); + FarseerBody = GameMain.World.CreateRectangle(width, height, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; default: throw new NotImplementedException(bodyShape.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index 495b5fd86..e5bf41bca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -45,6 +45,12 @@ namespace Barotrauma bool matchingElementFound = false; foreach (var subElement in element.Elements()) { + if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) + { + matchingElementFound = true; + elementsToRemove.AddRange(element.Elements()); + break; + } if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } if (i == index) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs index 1b40954db..c86e6050c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -20,9 +20,9 @@ namespace Barotrauma { if (!potentialCallFromConstructor) { return; } StackTrace st = new StackTrace(skipFrames: 2, fNeedFileInfo: false); - for (int i = st.FrameCount-1; i >= 0; i--) + for (int i = st.FrameCount - 1; i >= 0; i--) { - if (st.GetFrame(i)?.GetMethod() is {IsConstructor: true, DeclaringType: { } declaringType} + if (st.GetFrame(i)?.GetMethod() is { IsConstructor: true, DeclaringType: { } declaringType } && Types.Contains(declaringType)) { throw new Exception("Called disallowed method from within a prefab's constructor!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index e7c0cecab..36743762a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -79,7 +79,6 @@ namespace Barotrauma public override void Deselect() { base.Deselect(); - #if CLIENT var config = GameSettings.CurrentConfig; config.CrewMenuOpen = CrewManager.PreferCrewMenuOpen; @@ -88,6 +87,10 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); + if (GameMain.GameSession?.GameMode is TestGameMode) + { + DebugConsole.DeactivateCheats(); + } #endif } @@ -149,19 +152,19 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("GameSessionUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession", sw.ElapsedTicks); sw.Restart(); GameMain.ParticleManager.Update((float)deltaTime); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("ParticleUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles", sw.ElapsedTicks); sw.Restart(); if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("LevelUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); if (Character.Controlled != null) { @@ -193,7 +196,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("CharacterUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Character", sw.ElapsedTicks); sw.Restart(); #endif @@ -201,7 +204,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("StatusEffectUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:StatusEffects", sw.ElapsedTicks); sw.Restart(); if (Character.Controlled != null && @@ -253,7 +256,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("MapEntityUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity", sw.ElapsedTicks); sw.Restart(); #endif Character.UpdateAnimAll((float)deltaTime); @@ -266,7 +269,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("AnimUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Ragdolls", sw.ElapsedTicks); sw.Restart(); #endif @@ -277,7 +280,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("SubmarineUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Submarine", sw.ElapsedTicks); sw.Restart(); #endif @@ -297,7 +300,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Physics", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Physics", sw.ElapsedTicks); #endif UpdateProjSpecific(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 765a62374..0cae07ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -619,6 +619,11 @@ namespace Barotrauma if (parentObject is Powered powered) { value = powered.Voltage; return true; } } break; + case nameof(Powered.RelativeVoltage): + { + if (parentObject is Powered powered) { value = powered.RelativeVoltage; return true; } + } + break; case nameof(Powered.CurrPowerConsumption): { if (parentObject is Powered powered) { value = powered.CurrPowerConsumption; return true; } @@ -786,6 +791,9 @@ namespace Barotrauma case nameof(Character.HealthMultiplier): { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } + break; } return false; } @@ -799,6 +807,12 @@ namespace Barotrauma case nameof(Character.ObstructVision): { if (parentObject is Character character) { character.ObstructVision = value; return true; } } break; + case nameof(Character.HideFace): + { if (parentObject is Character character) { character.HideFace = value; return true; } } + break; + case nameof(Character.UseHullOxygen): + { if (parentObject is Character character) { character.UseHullOxygen = value; return true; } } + break; case nameof(LightComponent.IsOn): { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } break; @@ -1107,10 +1121,15 @@ namespace Barotrauma itemComponent.requiredItems.Clear(); itemComponent.DisabledRequiredItems.Clear(); - itemComponent.SetRequiredItems(element); + itemComponent.SetRequiredItems(element, allowEmpty: true); break; } - } + } + if (itemComponent is ItemContainer itemContainer && + (componentElement.GetChildElement("containable") != null || componentElement.GetChildElement("subcontainer") != null)) + { + itemContainer.ReloadContainableRestrictions(componentElement); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index d1aac1552..c21422874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -161,6 +161,7 @@ namespace Barotrauma RadialDistortion = true, InventoryScale = 1.0f, LightMapScale = 1.0f, + VisibleLightLimit = 50, TextScale = 1.0f, HUDScale = 1.0f, Specularity = true, @@ -200,6 +201,7 @@ namespace Barotrauma public float HUDScale; public float InventoryScale; public float LightMapScale; + public int VisibleLightLimit; public float TextScale; public bool RadialDistortion; } @@ -224,6 +226,7 @@ namespace Barotrauma { MusicVolume = 0.3f, SoundVolume = 0.5f, + UiVolume = 0.3f, VoiceChatVolume = 0.5f, VoiceChatCutoffPrevention = 0, MicrophoneVolume = 5, @@ -232,7 +235,6 @@ namespace Barotrauma UseDirectionalVoiceChat = true, VoipAttenuationEnabled = true, VoiceSetting = VoiceMode.PushToTalk, - UseLocalVoiceByDefault = false, DisableVoiceChatFilters = false }; return audioSettings; @@ -247,6 +249,7 @@ namespace Barotrauma public float MusicVolume; public float SoundVolume; + public float UiVolume; public float VoiceChatVolume; public int VoiceChatCutoffPrevention; public float MicrophoneVolume; @@ -262,7 +265,6 @@ namespace Barotrauma public string VoiceCaptureDevice; public float NoiseGateThreshold; - public bool UseLocalVoiceByDefault; public bool DisableVoiceChatFilters; } @@ -284,12 +286,13 @@ namespace Barotrauma { InputType.Aim, MouseButton.SecondaryMouse }, { InputType.InfoTab, Keys.Tab }, - { InputType.Chat, Keys.T }, - { InputType.RadioChat, Keys.R }, + { InputType.Chat, Keys.None }, + { InputType.RadioChat, Keys.None }, + { InputType.ActiveChat, Keys.T }, { InputType.CrewOrders, Keys.C }, { InputType.Voice, Keys.V }, - { InputType.LocalVoice, Keys.B }, + { InputType.ToggleChatMode, Keys.R }, { InputType.Command, MouseButton.MiddleMouse }, { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, { InputType.NextFireMode, MouseButton.MouseWheelUp }, @@ -301,7 +304,6 @@ namespace Barotrauma { InputType.Down, Keys.S }, { InputType.Left, Keys.A }, { InputType.Right, Keys.D }, - { InputType.ToggleInventory, Keys.Q }, { InputType.SelectNextCharacter, Keys.Z }, { InputType.SelectPreviousCharacter, Keys.X }, @@ -331,17 +333,35 @@ namespace Barotrauma if (!bindings.ContainsKey(inputType)) { bindings.Add(inputType, defaultBindings[inputType]); } } + bool playerConfigContainsNewChatBinds = false; foreach (XElement element in elements) { foreach (XAttribute attribute in element.Attributes()) { if (Enum.TryParse(attribute.Name.LocalName, out InputType result)) { + if (!playerConfigContainsNewChatBinds) + { + playerConfigContainsNewChatBinds = result == InputType.ActiveChat; + } bindings[result] = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); } } } + // Clear the old chat binds for configs saved before the introduction of the new chat binds + if (!playerConfigContainsNewChatBinds) + { + if (bindings.ContainsKey(InputType.Chat)) + { + bindings[InputType.Chat] = Keys.None; + } + if (bindings.ContainsKey(InputType.RadioChat)) + { + bindings[InputType.RadioChat] = Keys.None; + } + } + Bindings = bindings.ToImmutableDictionary(); } @@ -452,25 +472,31 @@ namespace Barotrauma public static void SetCurrentConfig(in Config newConfig) { - bool setGraphicsMode = - currentConfig.Graphics.Width != newConfig.Graphics.Width - || currentConfig.Graphics.Height != newConfig.Graphics.Height - || currentConfig.Graphics.VSync != newConfig.Graphics.VSync - || currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; - + bool resolutionChanged = + currentConfig.Graphics.Width != newConfig.Graphics.Width || + currentConfig.Graphics.Height != newConfig.Graphics.Height; bool languageChanged = currentConfig.Language != newConfig.Language; - bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice; bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice; - bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7); + bool hudScaleChanged = !MathUtils.NearlyEqual(currentConfig.Graphics.HUDScale, newConfig.Graphics.HUDScale); + + bool setGraphicsMode = + resolutionChanged || + currentConfig.Graphics.VSync != newConfig.Graphics.VSync || + currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; + currentConfig = newConfig; #if CLIENT if (setGraphicsMode) { - GameMain.Instance.ApplyGraphicsSettings(); + GameMain.Instance.ApplyGraphicsSettings(recalculateFontsAndStyles: true); + } + else if (textScaleChanged) + { + GUIStyle.RecalculateFonts(); } if (audioOutputChanged) @@ -483,12 +509,9 @@ namespace Barotrauma VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); } - if (textScaleChanged) + if (hudScaleChanged) { - foreach (var font in GUIStyle.Fonts.Values) - { - font.Prefabs.ForEach(p => p.LoadFont()); - } + HUDLayoutSettings.CreateAreas(); } GameMain.SoundManager?.ApplySettings(); @@ -504,23 +527,11 @@ namespace Barotrauma XElement graphicsElement = new XElement("graphicssettings"); root.Add(graphicsElement); currentConfig.Graphics.SerializeElement(graphicsElement); - -#region Backwards compatibility crap -#warning TODO: remove once modding refactor ships in a stable release - XElement backwardsCompatibilityGraphicsMode = new XElement(graphicsElement); root.Add(backwardsCompatibilityGraphicsMode); - backwardsCompatibilityGraphicsMode.Name = "graphicsmode"; -#endregion - + XElement audioElement = new XElement("audio"); root.Add(audioElement); currentConfig.Audio.SerializeElement(audioElement); XElement contentPackagesElement = new XElement("contentpackages"); root.Add(contentPackagesElement); -#region More backwards compatibility crap - XComment backwardsCompatibleComment = new XComment("Backwards compatibility"); contentPackagesElement.Add(backwardsCompatibleComment); -#warning TODO: remove once modding refactor ships in a stable release - XElement backwardsCompatibleCoreElement = new XElement("core"); contentPackagesElement.Add(backwardsCompatibleCoreElement); - backwardsCompatibleCoreElement.SetAttributeValue("name", "Vanilla 0.9"); -#endregion XComment corePackageComment = new XComment(ContentPackageManager.EnabledPackages.Core?.Name ?? "Vanilla"); contentPackagesElement.Add(corePackageComment); XElement corePackageElement = new XElement(ContentPackageManager.CorePackageElementName); contentPackagesElement.Add(corePackageElement); corePackageElement.SetAttributeValue("path", ContentPackageManager.EnabledPackages.Core?.Path ?? ContentPackageManager.VanillaFileList); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 025127e42..94e6c7bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -94,7 +94,13 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) { return; } + if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + if (!HasRequiredItems(entity)) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } if (!Stackable) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 45aa60f26..f86226e55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Xml.Linq; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 017a10301..3fae997c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -132,7 +132,11 @@ namespace Barotrauma public enum SpawnPositionType { This, + //the inventory of the StatusEffect's target entity ThisInventory, + //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + SameInventory, + //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) ContainedInventory } @@ -153,7 +157,7 @@ namespace Barotrauma /// Should the item spawn even if the container can't contain items of this type /// public readonly bool SpawnIfCantBeContained; - public readonly float Speed; + public readonly float Impulse; public readonly float Rotation; public readonly int Count; public readonly float Spread; @@ -194,7 +198,7 @@ namespace Barotrauma SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); - Speed = element.GetAttributeFloat("speed", 0.0f); + Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("speed", 0.0f)); Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); @@ -308,11 +312,26 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; + public float intervalTimer; + public static readonly List DurationList = new List(); - public readonly bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + public readonly bool CheckConditionalAlways; - public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? + /// + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// + public readonly bool Stackable = true; + + /// + /// The interval at which the effect is executed. The difference between delay and interval is that effects with a delay find the targets, check the conditions, etc + /// immediately when Apply is called, but don't apply the effects until the delay has passed. Effects with an interval check if the interval has passed when Apply is + /// called and apply the effects if it has, otherwise they do nothing. + /// + public readonly float Interval; #if CLIENT private readonly bool playSoundOnRequiredItemFailure = false; @@ -320,7 +339,7 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem, removeCharacter, breakLimb, hideLimb; + private readonly bool removeItem, dropContainedItems, removeCharacter, breakLimb, hideLimb; private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -450,6 +469,8 @@ namespace Barotrauma TargetSlot = element.GetAttributeInt("targetslot", -1); + Interval = element.GetAttributeFloat("interval", 0.0f); + Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); @@ -556,6 +577,7 @@ namespace Barotrauma " - sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "delay": + case "interval": break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -608,6 +630,9 @@ namespace Barotrauma case "removeitem": removeItem = true; break; + case "dropcontaineditems": + dropContainedItems = true; + break; case "removecharacter": removeCharacter = true; break; @@ -1091,6 +1116,12 @@ namespace Barotrauma { if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + currentTargets.Clear(); foreach (ISerializableEntity target in targets) { @@ -1192,7 +1223,11 @@ namespace Barotrauma lifeTimer -= deltaTime; if (lifeTimer <= 0) { return; } } - + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); if (useItemCount > 0) @@ -1224,6 +1259,22 @@ namespace Barotrauma } } + if (dropContainedItems) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) + { + foreach (var itemContainer in item.GetComponents()) + { + foreach (var containedItem in itemContainer.Inventory.AllItemsMod) + { + containedItem.Drop(dropper: null); + } + } + } + } + } if (removeItem) { for (int i = 0; i < targets.Count; i++) @@ -1657,7 +1708,7 @@ namespace Barotrauma throw new NotImplementedException("Spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); } body.SetTransform(newItem.SimPosition, rotation); - body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Impulse); } } newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; @@ -1698,6 +1749,26 @@ namespace Barotrauma } } break; + case ItemSpawnInfo.SpawnPositionType.SameInventory: + { + Inventory inventory = null; + if (entity is Character character) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + inventory = item.ParentInventory; + } + if (inventory != null) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + }); + } + } + break; case ItemSpawnInfo.SpawnPositionType.ContainedInventory: { Inventory thisInventory = null; @@ -1737,6 +1808,8 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + intervalTimer = Interval; + static Character CharacterFromTarget(ISerializableEntity target) { Character targetCharacter = target as Character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 09e9c36ce..7db7254a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -324,7 +324,7 @@ namespace Barotrauma.Steam using (var copyIndicator = new CopyIndicator(copyIndicatorPath)) { - await CopyDirectory(itemDirectory, modPathDirName ?? modName, itemDirectory, installDir); + await CopyDirectory(itemDirectory, modPathDirName ?? modName, itemDirectory, installDir, ShouldCorrectPaths.Yes); string fileListDestPath = Path.Combine(installDir, ContentPackage.FileListFileName); XDocument fileListDest = XMLExtensions.TryLoadXml(fileListDestPath); @@ -358,12 +358,15 @@ namespace Barotrauma.Steam string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: false); + bool isPath = false; + //Handle mods that have been mangled by pre-modding-refactor //copying of post-modding-refactor mods (what a clusterfuck) int modDirStrIndex = val.IndexOf(ContentPath.ModDirStr, StringComparison.OrdinalIgnoreCase); if (modDirStrIndex >= 0) { val = val[modDirStrIndex..]; + isPath = true; } //Handle really old mods (0.9.0.4-era) that might be structured as @@ -372,6 +375,7 @@ namespace Barotrauma.Steam if (File.Exists(fullSrcPath)) { val = $"{ContentPath.ModDirStr}/{val}"; + isPath = true; } //Handle old mods that installed to the fixed Mods directory @@ -380,6 +384,7 @@ namespace Barotrauma.Steam if (val.StartsWith(oldModDir, StringComparison.OrdinalIgnoreCase)) { val = $"{ContentPath.ModDirStr}{val.Remove(0, oldModDir.Length)}"; + isPath = true; } //Handle old mods that depend on other mods else if (val.StartsWith("Mods/", StringComparison.OrdinalIgnoreCase)) @@ -387,13 +392,15 @@ namespace Barotrauma.Steam string otherModName = val.Substring(val.IndexOf('/')+1); otherModName = otherModName.Substring(0, otherModName.IndexOf('/')); val = $"{string.Format(ContentPath.OtherModDirFmt, otherModName)}{val.Remove(0, $"Mods/{otherModName}".Length)}"; + isPath = true; } //Handle really old mods that installed Submarines in the wrong place else if (val.StartsWith("Submarines/", StringComparison.OrdinalIgnoreCase)) { val = $"{ContentPath.ModDirStr}/{val}"; + isPath = true; } - attribute.Value = val; + if (isPath) { attribute.Value = val; } } await Task.WhenAll( element.Elements() @@ -403,7 +410,7 @@ namespace Barotrauma.Steam element: subElement))); } - private static async Task CopyFile(string fileListDir, string modName, string from, string to) + private static async Task CopyFile(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { await Task.Yield(); Identifier extension = Path.GetExtension(from).ToIdentifier(); @@ -420,10 +427,14 @@ namespace Barotrauma.Steam { throw new Exception($"Could not load \"{from}\": doc is null"); } - await CorrectPaths( - fileListDir: fileListDir, - modName: modName, - element: doc.Root ?? throw new NullReferenceException()); + + if (shouldCorrectPaths == ShouldCorrectPaths.Yes) + { + await CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: doc.Root ?? throw new NullReferenceException()); + } doc.SaveSafe(to); return; } @@ -436,7 +447,12 @@ namespace Barotrauma.Steam File.Copy(from, to, overwrite: true); } - public static async Task CopyDirectory(string fileListDir, string modName, string from, string to) + public enum ShouldCorrectPaths + { + Yes, No + } + + public static async Task CopyDirectory(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { from = Path.GetFullPath(from); to = Path.GetFullPath(to); Directory.CreateDirectory(to); @@ -448,10 +464,10 @@ namespace Barotrauma.Steam string[] subDirs = Directory.GetDirectories(from); foreach (var file in files) { - await CopyFile(fileListDir, modName, file, convertFromTo(file)); + await CopyFile(fileListDir, modName, file, convertFromTo(file), shouldCorrectPaths); } - foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir)); } + foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index f8d4a1d2d..c193ba8d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -406,23 +406,14 @@ namespace Barotrauma } } - private void Dispose(bool disposing) + public void Dispose() { if (!Disposed) { - if (disposing) - { - TargetComponents.Clear(); - } + TargetComponents.Clear(); } Disposed = true; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 027279c9d..bbb727d01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -396,29 +396,20 @@ namespace Barotrauma return 1; } - private void Dispose(bool disposing) + public override void Dispose() { if (!disposed) { - if (disposing) - { - Prefabs.Remove(this); + Prefabs.Remove(this); #if CLIENT - Sprite?.Remove(); - Sprite = null; - DecorativeSprites.ForEach(sprite => sprite.Remove()); - targetProperties.Clear(); + Sprite?.Remove(); + Sprite = null; + DecorativeSprites.ForEach(sprite => sprite.Remove()); + targetProperties.Clear(); #endif - } } disposed = true; } - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs index 404e8b571..7a904fdac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs @@ -1,7 +1,11 @@ using System; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +[assembly:InternalsVisibleTo("WindowsTest"), + InternalsVisibleTo("MacTest"), + InternalsVisibleTo("LinuxTest")] public static class AssemblyInfo { public static readonly string GitRevision; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs index 9f0a2f711..3f80b5500 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs @@ -7,11 +7,6 @@ namespace Barotrauma { private readonly Dictionary> events = new Dictionary>(); - ~NamedEvent() - { - ReleaseUnmanagedResources(); - } - public void Register(Identifier identifier, Action action) { if (HasEvent(identifier)) @@ -53,15 +48,9 @@ namespace Barotrauma } } - private void ReleaseUnmanagedResources() + public void Dispose() { events.Clear(); } - - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index c9152e8c1..73c6d6860 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -15,6 +15,21 @@ namespace Barotrauma public bool IsNone() => this is None; public bool IsSome() => this is Some; + public bool TryUnwrap(out T outValue) + { + switch (this) + { + case Some { Value: var value }: + outValue = value; + return true; + case None _: + outValue = default; + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + public Option Select(Func selector) => this switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 2a1585e5b..eb17b94a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -7,9 +7,14 @@ namespace Barotrauma { public static class ReflectionUtils { + private static Type[] cachedNonAbstractTypes; public static IEnumerable GetDerivedNonAbstract() { - return Assembly.GetEntryAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract); + if (cachedNonAbstractTypes == null) + { + cachedNonAbstractTypes = Assembly.GetEntryAssembly().GetTypes().Where(t => !t.IsAbstract).ToArray(); + } + return cachedNonAbstractTypes.Where(t => t.IsSubclassOf(typeof(T))); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index b63e7a2bc..bb1950e0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -11,8 +11,8 @@ namespace Barotrauma public static Success Success(T value) => new Success(value); - public static Failure Failure(TError error, string? stackTrace) - => new Failure(error, stackTrace); + public static Failure Failure(TError error) + => new Failure(error); } public sealed class Success : Result @@ -34,14 +34,11 @@ namespace Barotrauma { public readonly TError Error; - public readonly string? StackTrace; - public override bool IsSuccess => false; - public Failure(TError error, string? stackTrace) + public Failure(TError error) { Error = error; - StackTrace = stackTrace; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 573e248ab..651f13975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -18,6 +18,23 @@ namespace Barotrauma.IO ".bat", ".sh", //shell scripts }.ToIdentifiers().ToImmutableArray(); + public ref struct Skipper + { + public void Dispose() + { + SkipValidationInDebugBuilds = false; + } + } + + /// + /// Skips validation for as long as the returned object remains in scope (remember to use using) + /// + public static Skipper SkipInDebugBuilds() + { + SkipValidationInDebugBuilds = true; + return new Skipper(); + } + /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. /// @@ -429,8 +446,8 @@ namespace Barotrauma.IO public class FileStream : System.IO.Stream { - private System.IO.FileStream innerStream; - private string fileName; + private readonly System.IO.FileStream innerStream; + private readonly string fileName; public FileStream(string fn, System.IO.FileStream stream) { @@ -496,9 +513,9 @@ namespace Barotrauma.IO innerStream.Flush(); } - protected override void Dispose(bool disposing) + protected override void Dispose(bool notCalledByFinalizer) { - innerStream.Dispose(); + if (notCalledByFinalizer) { innerStream.Dispose(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index b55e03a9f..6ca456993 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -126,6 +126,10 @@ namespace Barotrauma public static void LoadGame(string filePath) { + //ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub) + //can happen if a gamesession is interrupted ungracefully (exception during loading) + Submarine.Unload(); + GameMain.GameSession = null; DebugConsole.Log("Loading save file: " + filePath); DecompressToDirectory(filePath, TempPath, null); @@ -479,8 +483,8 @@ namespace Barotrauma { int read = 0; - // FIXME workaround for .NET6 causing save decompression to fail -#if NET6_0 && LINUX + // BUG workaround for .NET6 causing save decompression to fail +#if NET6_0 for (int i = 0; i < amount; i++) { int result = zipStream.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs index 921725254..f2b0fceb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs @@ -40,10 +40,14 @@ namespace Barotrauma } } - private static void AddInternal(string name, Task task, Action onCompletion, object userdata) + private static void AddInternal(string name, Task task, Action onCompletion, object userdata, bool addIfFound = true) { lock (taskActions) { + if (!addIfFound) + { + if (taskActions.Any(t => t.Name == name)) { return; } + } if (taskActions.Count >= MaxTasks) { throw new Exception( @@ -59,6 +63,10 @@ namespace Barotrauma { AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null); } + public static void AddIfNotFound(string name, Task task, Action onCompletion) + { + AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null, addIfFound: false); + } public static void Add(string name, Task task, U userdata, Action onCompletion) where U : class { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index e165d24e7..9922dc37f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -100,13 +100,23 @@ namespace Barotrauma } #endif - if (Path.IsPathRooted(originalFilename)) + string startPath = directory ?? ""; + + string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/'); + if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { + //paths that lead to the save folder might have incorrect case, + //mainly if they come from a filelist + startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/"; + filename = startPath; + subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray(); + } + else if (Path.IsPathRooted(originalFilename)) + { + #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing. return originalFilename; //assume that rooted paths have correct case since these are generated by the game } - string startPath = directory ?? ""; - for (int i = 0; i < subDirs.Length; i++) { if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i])) diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 292474ecf..bc2dec55c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,220 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.11.0 +--------------------------------------------------------------------------------------------------------- + +- Disabled project-wide invariant globalization, which was meant to address "couldn't find a valid ICU package installed on the system" errors on some Linux distributions. The fix caused issues with case-insensitive comparisons and converting to upper or lower case in non-latin alphabets. +- Fixed tutorial characters spawning without a headset. +- Fixed inability to bind keys to LMB by clicking on the input box. + +--------------------------------------------------------------------------------------------------------- +v0.18.10.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "index out of range" error when joining a server where's someone's rewired items. +- Fixed some mission messages being displayed as "missionmessage0.nameofthemission". + +--------------------------------------------------------------------------------------------------------- +v0.18.9.0 +--------------------------------------------------------------------------------------------------------- + +- Updated translations. +- Fixed crashing when a mod includes event sets that reference a non-existent event. +- Backwards compatibility: readded some mission events we'd removed since the previous update. + +--------------------------------------------------------------------------------------------------------- +v0.18.8.0 +--------------------------------------------------------------------------------------------------------- + +Balancing: +- Added some new campaign settings: starting balance, amount of starting items and difficulty. +- Cargo mission reward of construction materials has been reduced to be less balance-breaking. +- Revisited all item spawns. Drastically reduced and adjusted the spawns everywhere. Disabled some spawns in campaign. All the subs should now start with a bare minimum in the campaign. +- Reduced selling price to ~25% of base price to avoid getting too rich from looting too early/easily +- Increased effect of "Requested Goods" to be 2x to compensate for the decreased selling price. +- Alien artifacts and trinkets can still be sold for a high price at research stations. (2x modifier, to compensate for the reduced selling price) +- Removed batteries from Headset, to reduce the value of selling/deconstructing these. +- Duffelbag deteriorates over time when in use, and now is carried with both hands. +- All items now deconstruct into less materials than it takes to construct them. Avoiding infinite construction/deconstruction loops for easy skill leveling. +- (Temporarily?) Removed most hand-placed items from the vanilla subs to make balancing and debugging the auto item placement easier. +- Revisited crew corpse spawns. The id cards are no longer manually placed. The cards found from the crew now actually work. +- Minor adjustments to bandit loadouts. +- Changes to chaingun. Now fires 500 shots instead of 200 per ammo box, at the cost of DPS. +- Added shredder rounds for chaingun, as an option against armoured targets. +- Adjusted the armor penetration of all turrets. +- Made location evolution take a little longer, colonies cannot be formed closer than three steps to another colony. +- Made wreck missions a little more common. +- Adjustments to the preferred containers (= where things are spawned and where they should be placed). +- Changes to the existing missions and how they are distributed. Added new missions. +- Reduced the costs for unlocking the biomes. +- Adjustments to the monster spawns. +- Changes to the item "gating". Some items don't appear early in the game anymore. +- Adjustments to the mission specific variants of the monsters. +- Added a large Crawler variant for some missions (removed the Swarmcrawler that was used for crawler missions). +- Halved Mudraptors' priority for eating dead bodies. +- Reduce nausea chance of energy drink to 25%. +- Changes to the campaign progression in general. +- Changes to the level generation parameters, especially in Cold Caverns and the Ridge. +- Changes to the level resources distribution. +- Changes to the event manager settings (that affect the monster spawns). +- Adjusted and normalized the item loadouts for all the jobs. + +Chat improvements: +- Chat mode (radio/local) can be toggled using a dropdown next to the chat box or with a dedicated "ToggleChatMode" keybind (R by default). +- Voice chat now has only one push-to-talk keybind (V by default) which respects the selected chat mode. +- There's now a dedicated "ActiveChat" keybind (T by default) to open the chat using the currently active chat mode. +- If you want to keep the chat keybinds the way they were (separate keybinds for local and radio), you can rebind the "Chat" and "RadioChat" inputs back to T and R and the new "ToggleChatMode" and "ActiveChat" inputs to something else. + +Changes and additions: +- Added damage overlays to characters (characters who've taken damage look damaged). +- Added two new beacon stations. +- Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). +- Added UI volume slider. +- Show a verification prompt if an automated circuit tries to make the submarine undock from or dock with an outpost. Prevents campaign getting softlocked if someone rewires the docking port in a way that makes it dock/undock immediately at the start of around. +- Color subs in the sub editor's list to indicate whether they're vanilla, workshop or local subs, added a tooltip that explains why some of them cannot be deleted through the editor. +- ID cards can now be purchased from outposts. The card gets assigned the appropriate tags for the character doing the purchase. +- Clients need to wait 1 minute if their vote gets rejected before they can start another vote of the same type. +- Increased the priority of explosion particles to make it less likely for them to not appear when the particle limit has been reached. +- Made matriarch genes slowly heal bleeding (not just afflictions of the type "damage") to get it to be more in line with the description. +- Adjusted small water flow sounds: lower max volume, lerp volume according to the water flow (-> small leaks are much more quiet). +- Added energy drinks and protein bars to vending machines. +- Reduced Winterhalter engine power drain (from 6000 total to 4250). +- Decorative level objects (plants and whatnot) can spawn on outpost walls. +- Adjustments on the particle effects of chaingun and coilgun. +- Added non-lethal rubber bullets for riot shotgun. +- Added a server setting to change if the looted money goes to the player or to the bank. +- Improved tooltips in the wallet menu to make their function more clear. +- Corpses can now be grabbed in singleplayer to loot money. +- Made the crew wallet menu update when the players permissions change. +- Prevented selling items from submarine containers tagged with "dontsellitems", instead of "donttakeitems". +- Removed merchant balance effect on item prices. +- Replaced "item sell value" with the location reputation effect on the store interface. +- Hide AppData path from tooltips in the sub editor to prevent exposing the user's name. +- Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. +- Increased oxygen generator output in some vanilla subs. +- Made handheld sonar beacon sound less grating. +- The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). +- Made flashlight flicker before the battery runs out. +- Added some lootable money to corpses found in wrecks. +- Removed the small equipment indicators next to the character portrait. +- Weapon holders now use the tag "mountableweapon" instead of "weapon" to determine which items can be placed in them. Allows tagging non-weapon items as mountable in the holder, without making bots consider it a weapon due to the "weapon" tag. Also allows to keep some weapons not-mountable. +- Ammunition Shelf can now also store Depth Charges ("depthchargeammo" tag added) +- Doors and hatches can now be mirrored in the sub editor (making them open from top to bottom, or from right to left). +- Depth charges can be stored in coilgun ammo shelves. +- Adding preview images to wrecks, beacon stations, outposts or enemy subs isn't allowed in the sub editor (unnecessarily bloats up their file size, as the preview images aren't visible anywhere). + +Performance: +- Improved the performance statistics view that's enabled with the "showperf" console command: more fine-grained stats and easier-to-read visuals. +- Optimized AI pathfinding when they're trying to find a safe hull. Particularly noticeable in colonies when the NPCs are fleeing from something. +- Optimized character status effects (e.g. health regen and other constant damage reductions). +- Optimized watcher's acid clouds. +- Optimized loading submarines. Reduces loading times especially when there's lots of items in the sub. +- Lighting optimization: now some unimportant (dim and small) lights are hidden when there's lots of light sources visible on the screen at the same time. The maximum number of visible lights can be adjusted in the game settings. +- Lighting optimization: the number of light recalculations per frame is limited, meaning that when there's lots of moving, shadow-casting lights visible, the game doesn't try to recalculate the shadows all at the same time. +- Lighting optimization: simplify the light rendering when zoomed very far out (e.g. when looking through a periscope). +- Optimized status effects that modify items' conditions every frame (for example, oxygen tank shelves that fill up oxygen tanks). +- Optimized many status effects by making them only execute once per second instead of every frame (most importantly, diving suits and volatile fuel rods). +- Optimized talents: buffs are applied to characters periodically instead of every frame. +- Optimized the logic that bots use to determine the safety of hulls. +- Optimized items: stop updating items that don't need to be updated more aggressively. +- Optimized bot AIs: in particular, the cleanup, repair, pump water and load items objectives. Should significantly improve performance when the bots are doing these objectives when there's a large number of items in the sub. +- Optimized entity culling logic (determines which items/structures are currently visible in the screen). +- Optimized a bunch of textures. + +Fixes: +- Fixed server not refreshing the power grid when a client disconnects and reconnects a power wire. +- Fixed hull updates not being sent if the water/oxygen/fire in the hull doesn't change server-side, preventing the hull's status from getting corrected if a client somehow ends up out of sync. +- Fixed ballast flora sometimes becoming unkillable in multiplayer. +- Attempt to fix tab menu crew list sometimes getting stuck to a broken state at the beginning of a round. +- Fixed inability to access the character tab in the tab menu when dead (preventing you from creating a new character). +- Fixed occasional "hash calculation for content package xxxx didn't match expected hash" errors when updating/enabling certain mods. +- Fixed preview sometimes breaking in the character customization menu when switching the hair or accessories on Linux or Mac. +- Fixed fonts not getting rescaled when changing resolution. +- Fixed misplaced hull in the beacon stations. +- Fixed ability to pick up items and take items from other characters when controlling a character whose inventory is inaccessible while alive. +- Fixed message box about a too large preview image not being shown when trying to publish one in the Workshop (instead throwing the generic "publishing failed" error). +- Fixed Venture airlock (missing button, inner door wiring). +- Fixed level floor not being visible on the sonar. +- Fixed bots being unable to shoot with a turret whose line of sight is blocked by another turret (even though the projectiles can go through the turret). +- Fixed switching a sub making its preview image disappear from the submarine switch menu. +- Fixed an issue where the client was adding mission rewards into the bank on their screen causing desync. +- Fixed item assemblies still getting misaligned when saving. +- Fixed crashing when there's no audio device available (no speakers/headset connected) and a character enters water. +- Fixed crashing when trying to save an item assembly with a space at the end of the name. +- Fixed crashing when a character tries to operate a turret from outside the sub. +- Fixed submarine name being set to a truncated value in the submarine save dialog if the submarine name text at the top of the screen gets truncated, leading to a crash if you try to save the sub with that name. +- Fixed devices whose power consumption is set to 0 not working when not connected to a grid. +- Fixed outpost NPCs choosing the item to spawn for the device they're operating randomly, occasionally causing them to for example load reactors with volatile rods. +- Clients replicate sending chat messages to wifi components in mp. Fixes radio-linked wifi components not receiving the signals client-side. +- Fixed tab menu staying open during loading screens. +- Signal components' and terminals' sprites don't mirror horizontally in mirrored subs (what's a DNA, RO, ROX or XEGER component??). +- Fixed inability to rewire any docking ports in outpost levels, even if the port is not docked with anything (should only apply to the port docked with the outpost). +- Fixed "Ignore This" orders being wiped when loading an existing multiplayer campaign save. +- Fixed abyss area being very small in the Aphotic Plateau, preventing the abyss monster from reaching you if you go deep enough. +- Fixed status monitor displaying small amounts of water as 1% even though water detectors output 0%. +- Fixed autopilot conflicting with VELOCITY_IN inputs (now signals override the autopilot for 1 second). +- Fixed ConversationAction getting interrupted when opening an input-blocking menu in single player. +- Fixed sprite bleed in chaingun ammunition boxes. +- Fixed appearance of specific named NPCs being inconsistent (e.g. Captain Hognose sometimes being a woman or not having an eyepatch). +- Fixed certain scripted events getting stuck if you switch characters in single player (e.g. the events that require you to interact with fliers on the wall). +- Fixed crashing when the source of a rope is removed (e.g. when a latcher despawns while latched on to the sub). +- Fixed votes always going through if no-one votes. +- Fixed energy drink giving x10 more haste when used via the health interface. +- Fixed the monster spawns for the new game plus not working (currently a placeholder set). +- Fixed monsters spawning from missions not avoiding the engines. +- Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. +- Fixed some pumps in Kastrull working without power. +- Fixed quick-reloading working incorrectly when trying to reload from a stack that doesn't fully fit in the weapon (e.g. when double clicking on a full stack of revolver rounds with a half-loaded revolver in hand). +- Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). +- Fixed outpost NPCs having x3 more health than they should. +- Fixed morbusine not killing NPCs with higher-than-default health. +- Fixed graphics errors when using Razer Cortex overlay. +- Fixed bots being unable to repair Winterhalter's top hatch. +- Fixed server crashing if you disable all mission types and try to start a mission round. +- Fixed Chinese/Japanese/Korean text not wrapping properly on terminals. +- Fixed bots sometimes walking towards a wall or holding the ladders when they are idling. +- Fixed "main docking port" property not being taken into account when placing outposts (= the outpost was placed with the assumption that the docking port closest to the sub's center is the main docking port). Sometimes caused the outpost to be placed too close to the level walls, preventing the sub from docking with it. +- Fixed ladders not being visible in the sub preview. +- Fixed some UI elements being too large when switching from a large resolution to a smaller one, or vice versa. +- Fixed weapon holder sprite depth. +- Fixed level editor's test mode generating a different level than the editor itself. +- Fixed ballast flora branches that have been disconnected from the root not being considered disconnected after a level transition (allowing them to keep growing). +- Fixed "set default bindings" not doing anything in the settings menu. +- Fixed door/hatch gaps not getting moved when snapping to grid in the sub editor. +- Vertically mirrored beds can't be laid on. +- Fixed wrecked reactors being forced to non-interactable even if made interactable in the sub editor. +- Fixed keybinds shown in the controls tab not refreshing when resetting the binds. +- Hopefully fixed colonies sometimes not including some modules (most often the armory module). +- Fixed ready checks sometimes ending at a slightly different time client-side compared to the server, allowing you to answer the prompt even though the time to answer already ended server-side. +- Fixed large terminal welcome messages going slightly outside the bounds of the listbox. +- Fixed overlapping in the tab menu's mission tab when there's more than one mission selected. +- Fixed fabricators and deconstructors playing the sounds even if they're out of power. +- Fixed occasional "hash mismatch for downloaded mod" errors on Linux. +- Fixed clients occasionally spawning as the old character after they've opted to create a new one. Only happened if the client hadn't died and was still controlling the old character at the end of the round. +- When a client creates a character with a new name, the client's name is changed to match it after they spawn as that character. +- Fixed enabled mods getting disabled when updating them in the mods menu. +- Fixed a rounding error in Sprite.DrawTiled that sometimes caused an extra 1-pixel line on some scaled and flipped structures (e.g. certain wall pieces scaled to 0.6). +- Fixed Orca 2 still using the old chaingun charge time. + +Modding: +- Added "mod lists" which can be used to enable/disable sets of mods more easily. +- Option to choose which local mod(s) to add a submarine to when saving one in the submarine editor. +- Mods can be unsubscribed from by right-clicking on them in the mod list, and it's possible to unsubscribe from multiple ones at the same time by using ctrl+click or shift+click to select more than one. +- Local mods can be merged in the mod list by selecting the ones you want to merge and selecting "merge all selected" from the right-click context menu. +- Better filtering in the mod list: option to only show local mods, Workshop mods, published mods, submarines and/or item assemblies. +- Added "SameInventory" spawn position type to status effects (allows spawning items in the same inventory the entity applying the effect is in). +- Added support for multiple light components in wearables. +- Fixed permanent stats given by talents not getting synced to clients in multiplayer (doesn't affect any vanilla talents). +- Fixed nullref exception when trying to trigger a location type change to a type that doesn't exist (doesn't happen in the vanilla game). +- Added an extra tag to the "canned heat" talent to make it easier to add custom upgradeable tanks that aren't compatible with vanilla tools. +- Option to make status effects drop the items contained inside the target item (usage example in the duffel bag). +- Level object, cave and mineral commonness can be defined based on the biome instead of the level generation parameters (= no need to define commonness for "coldcavernsbasic", "coldcavernsmaze" etc separately). +- Option to define ConversationAction texts directly in the event xml (instead of having to always define them in a spearate text file). +- Extended CustomInterface functionality with NumberInput elements that allow using float values ("numbertype") and defining the increment size ("step") the number of decimal places ("decimalplaces"). (Thanks, mLuby!) +- Implemented element for removing all the child elements of an element in a variant file. +- TriggerComponent now supports negative forces: negative force value will cause the it to pull triggerers towards it. +- Multiple TriggerComponent properties can now be modified through signals and CustomInterface components. + + --------------------------------------------------------------------------------------------------------- v0.17.16.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 34f4c94b2..50ed25dac 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -52,4 +52,6 @@ missiontype="Random" autobantime="3600" maxautobantime="86400" - /> \ No newline at end of file + lootedmoneydestination="Bank" + maximumtransferrequest="999999" +/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs new file mode 100644 index 000000000..83cedcc37 --- /dev/null +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -0,0 +1,205 @@ +#nullable enable + +using System; +using Barotrauma; +using Barotrauma.Networking; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using Xunit; + +namespace TestProject +{ + // ReSharper disable UnusedMember.Local NotAccessedField.Local UnusedMember.Global + public class INetSerializableStructTests + { + private class CustomGenerators + { + // no null strings!!! + public static Arbitrary StringGeneratorOverride() => Arb.Default.String().Generator.Where(s => s != null).ToArbitrary(); + } + + public INetSerializableStructTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void TestOptional() + { + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestNested() + { + Prop.ForAll((arg1, arg2, arg3) => SerializeDeserializeNullableTuple(arg1, new TupleNullableStruct { One = arg2, Two = arg3 })).QuickCheckThrowOnFailure(); + Prop.ForAll((arg1, arg2) => SerializeDeserialize(new TupleNullableStruct { One = arg1, Two = arg2 })).QuickCheckThrowOnFailure(); + Prop.ForAll((arg1, arg2) => SerializeDeserialize(new TupleNullableStruct { One = arg1, Two = arg2 })).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestVector2() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestColor() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestEnum() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestArray() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestNullable() + { + Prop.ForAll(SerializeDeserializeNullableTuple).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestBoolean() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestByte() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt16() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt16() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt32() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt32() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt64() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt64() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestSingle() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestDouble() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestString() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + private enum EnumTest + { + One = 1, + Two = 2, + Three = 3, + Thousand = 1000 + } + + private struct TestStruct : INetSerializableStruct + { + [NetworkSerialize] + public T Value; + + public T NotSerializedValue; + + public T NotSerializedFunction() => throw new NotImplementedException(); + } + + private struct TupleNullableStruct : INetSerializableStruct + { + [NetworkSerialize] + public T? One; + + [NetworkSerialize] + public U? Two; + + public (T, U) NotSerializedValue; + public (T, U) NotSerializedFunction() => throw new NotImplementedException(); + } + + private static void SerializeDeserialize(T arg) where T : notnull + { + ReadWriteMessage msg = new ReadWriteMessage(); + TestStruct writeStruct = new TestStruct + { + Value = arg + }; + + ((INetSerializableStruct)writeStruct).Write(msg); + msg.BitPosition = 0; + + TestStruct readStruct = INetSerializableStruct.Read>(msg); + + readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + } + + private static void SerializeDeserializeNullableTuple(T arg1, U arg2) + { + ReadWriteMessage msg = new ReadWriteMessage(); + TupleNullableStruct writeStruct = new TupleNullableStruct + { + One = arg1, + Two = arg2 + }; + + ((INetSerializableStruct)writeStruct).Write(msg); + msg.BitPosition = 0; + + TupleNullableStruct readStruct = INetSerializableStruct.Read>(msg); + + readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/LinuxTest.csproj b/Barotrauma/BarotraumaTest/LinuxTest.csproj new file mode 100644 index 000000000..821101d06 --- /dev/null +++ b/Barotrauma/BarotraumaTest/LinuxTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + LinuxTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaTest/MacTest.csproj b/Barotrauma/BarotraumaTest/MacTest.csproj new file mode 100644 index 000000000..20b02e7a3 --- /dev/null +++ b/Barotrauma/BarotraumaTest/MacTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + MacTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaTest/TestExample.cs b/Barotrauma/BarotraumaTest/TestExample.cs new file mode 100644 index 000000000..6c02360dd --- /dev/null +++ b/Barotrauma/BarotraumaTest/TestExample.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Xml.Linq; +using Barotrauma; +using FluentAssertions; +using FsCheck; +using FsCheck.Xunit; +using Microsoft.Xna.Framework; +using Xunit; +using Xunit.Abstractions; + +namespace TestProject +{ + public class TestExample + { + // By default FsCheck has generators for basic types like floats ints and strings + // Anything custom like Rectangle or Vector2 requires writing a custom generator for it + private class CustomExampleGenerators + { + // We override the float generator to exclude NaNs and infinites + public static Arbitrary FloatGeneratorOverride() => Arb.Default.Float32().Generator.Where(MathUtils.IsValid).ToArbitrary(); + + // We override the String generator to exclude null and empty strings + public static Arbitrary StringGeneratorOverride() => Arb.Default.String().Generator.Where(s => !string.IsNullOrWhiteSpace(s)).ToArbitrary(); + + // Generator for the Rectangle type + public static Arbitrary RectangleGenerator() + { + return Arb.From(from int x in Arb.Generate() + from int y in Arb.Generate() + from int w in Arb.Generate().Where(i => i > 0) + from int h in Arb.Generate().Where(i => i > 0) + select new Rectangle(x, y, w, h)); + } + } + + // Used to output text into the test output + private readonly ITestOutputHelper testOutputHelper; + + public TestExample(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + Arb.Register(); // Register our custom generators + } + + [Fact] // Create a public function and add the [Fact] attribute on it to make a test function + public void TestXORAlgorithm() + { + Prop.ForAll((text, key) => // generates a pair of random strings + { + string encrypted = XOREncryptDecrypt(text, key); + string decrypted = XOREncryptDecrypt(encrypted, key); + + decrypted.Should().BeEquivalentTo(text); // FluentAssertions provides clear and verbose assertions with the Should() method + }).VerboseCheckThrowOnFailure(testOutputHelper); + // VerboseCheck performs 100 tests and outputs the generated values into the test output + // ThrowOnFailure will additionally throw an exception if any of the functions fail which will make the test fail + + // We will see that this fails the test with the following exception: + + /* + * System.Exception + * Falsifiable, after 1 test (0 shrinks) (StdGen (2118948508,297004609)): + * Original: + * ("Jl", "m") + * with exception: + * Xunit.Sdk.XunitException: Expected decrypted to be equivalent to "Jl" with a length of 2, but "948492" has a length of 6, differs near "948" (index 0). + */ + + // We can see that the reason it is failing is because the original text was "Jl" but when encrypted and decrypted the string becomes "948492" + // This is of course because we are not casting the XOR'd value to a char and instead appending the integer to the string builder + } + + // Erroneous XOR encryption algorithm + private static string XOREncryptDecrypt(string text, string key) + { + var result = new StringBuilder(); + + for (int i = 0; i < text.Length; i++) + { + result.Append(text[i] ^ (uint)key[i % key.Length]); + } + + return result.ToString(); + } + + private class ExampleEntity : ISerializableEntity + { + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float ExampleValue { get; set; } + + public string Name => nameof(ExampleEntity); + public Dictionary SerializableProperties { get; } + + public ExampleEntity() + { + SerializableProperties = SerializableProperty.GetProperties(this); + } + } + + [Fact] + public void TestPropertyConditionalEqualsOperator() + { + // Test if the PropertyConditional equals operator is working correctly + Prop.ForAll(value => + { + XAttribute xmlAttribute = new XAttribute("examplevalue", $"equals {value}"); + + PropertyConditional conditional = new PropertyConditional(xmlAttribute); + + ExampleEntity entity = new ExampleEntity + { + ExampleValue = value + }; + + conditional.Matches(entity).Should().BeTrue(); + }).VerboseCheckThrowOnFailure(testOutputHelper); // Remember to pass testOutputHelper so we actually get output + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs new file mode 100644 index 000000000..614b0b23b --- /dev/null +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -0,0 +1,34 @@ +using Barotrauma; +using FsCheck; +using Microsoft.Xna.Framework; + +namespace TestProject +{ + public static class TestProject + { + public class CustomGenerators + { + public static Arbitrary Vector2Generator() + { + return Arb.From(from int x in Arb.Generate() + from int y in Arb.Generate() + select new Vector2(x, y)); + } + + public static Arbitrary ColorGenerator() + { + return Arb.From(from int r in Gen.Choose(0, 255) + from int g in Gen.Choose(0, 255) + from int b in Gen.Choose(0, 255) + select new Color(r, g, b)); + } + + public static Arbitrary> OptionalGenerator() + { + return Arb.From(from T x in Arb.Generate() + from bool isNone in Arb.Generate() + select x is null || isNone ? Option.None() : Option.Some(x)); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/WindowsTest.csproj b/Barotrauma/BarotraumaTest/WindowsTest.csproj new file mode 100644 index 000000000..f7c2fc3be --- /dev/null +++ b/Barotrauma/BarotraumaTest/WindowsTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + WindowsTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs b/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs index ac937096b..21e218c7b 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs @@ -44,13 +44,13 @@ namespace FarseerPhysics.Common if (path.Closed) { ChainShape chain = new ChainShape(verts, true); - body.CreateFixture(chain); + body.CreateFixture(chain, Category.Cat1, Category.All); } else { for (int i = 1; i < verts.Count; i++) { - body.CreateFixture(new EdgeShape(verts[i], verts[i - 1])); + body.CreateFixture(new EdgeShape(verts[i], verts[i - 1]), Category.Cat1, Category.All); } } } @@ -74,7 +74,7 @@ namespace FarseerPhysics.Common foreach (Vertices item in decomposedVerts) { - body.CreateFixture(new PolygonShape(item, density)); + body.CreateFixture(new PolygonShape(item, density), Category.Cat1, Category.All); } } @@ -105,7 +105,7 @@ namespace FarseerPhysics.Common foreach (Shape shape in shapes) { - b.CreateFixture(shape); + b.CreateFixture(shape, Category.Cat1, Category.All); } bodyList.Add(b); diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs index 9f367a27a..4d988c318 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs @@ -56,7 +56,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Vertices part in vertices) { PolygonShape polygonShape = new PolygonShape(part, density); - Fixture fixture = MainBody.CreateFixture(polygonShape); + Fixture fixture = MainBody.CreateFixture(polygonShape, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -67,7 +67,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Shape part in shapes) { - Fixture fixture = MainBody.CreateFixture(part); + Fixture fixture = MainBody.CreateFixture(part, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -82,7 +82,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Vertices part in triangles) { PolygonShape polygonShape = new PolygonShape(part, density); - Fixture fixture = MainBody.CreateFixture(polygonShape); + Fixture fixture = MainBody.CreateFixture(polygonShape, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -161,7 +161,7 @@ namespace FarseerPhysics.Common.PhysicsLogic Body body = World.CreateBody(MainBody.Position, MainBody.Rotation, BodyType.Dynamic); body.UserData = MainBody.UserData; - Fixture newFixture = body.CreateFixture(shape); + Fixture newFixture = body.CreateFixture(shape, Category.Cat1, Category.All); newFixture.UserData = fixtureTag; Parts[i] = newFixture; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs b/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs index 35f6535b9..5df05e24c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs @@ -614,7 +614,7 @@ namespace FarseerPhysics.Common { foreach (XMLFragmentElement element in fixtureElement.Elements) { - Fixture fixture = new Fixture(); + Fixture fixture = new Fixture(Category.Cat1, Category.All); if (element.Name.ToLower() != "fixture") throw new Exception(); diff --git a/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs b/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs index 02ed6f58f..67dd7621c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs +++ b/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs @@ -35,7 +35,7 @@ namespace FarseerPhysics.Content foreach (FixtureTemplate fixtureTemplate in Fixtures) { - Fixture fixture = body.CreateFixture(fixtureTemplate.Shape); + Fixture fixture = body.CreateFixture(fixtureTemplate.Shape, Category.Cat1, Category.All); fixture.UserData = fixtureTemplate.Name; fixture.Restitution = fixtureTemplate.Restitution; fixture.Friction = fixtureTemplate.Friction; diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs index 69769aabe..389c100c1 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs @@ -27,76 +27,76 @@ namespace FarseerPhysics.Dynamics /// The shape. /// Application specific data /// - public virtual Fixture CreateFixture(Shape shape) + public virtual Fixture CreateFixture(Shape shape, Category collisionCategory, Category collidesWith) { - Fixture fixture = new Fixture(shape); + Fixture fixture = new Fixture(shape, collisionCategory, collidesWith); Add(fixture); return fixture; } - public Fixture CreateEdge(Vector2 start, Vector2 end) + public Fixture CreateEdge(Vector2 start, Vector2 end, Category collisionCategory, Category collidesWith) { EdgeShape edgeShape = new EdgeShape(start, end); - return CreateFixture(edgeShape); + return CreateFixture(edgeShape, collisionCategory, collidesWith); } - public Fixture CreateChainShape(Vertices vertices) + public Fixture CreateChainShape(Vertices vertices, Category collisionCategory, Category collidesWith) { ChainShape shape = new ChainShape(vertices); - return CreateFixture(shape); + return CreateFixture(shape, collisionCategory, collidesWith); } - public Fixture CreateLoopShape(Vertices vertices) + public Fixture CreateLoopShape(Vertices vertices, Category collisionCategory, Category collidesWith) { ChainShape shape = new ChainShape(vertices, true); - return CreateFixture(shape); + return CreateFixture(shape, collisionCategory, collidesWith); } - public Fixture CreateRectangle(float width, float height, float density, Vector2 offset) + public Fixture CreateRectangle(float width, float height, float density, Vector2 offset, Category collisionCategory, Category collidesWith) { Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2); rectangleVertices.Translate(ref offset); PolygonShape rectangleShape = new PolygonShape(rectangleVertices, density); - return CreateFixture(rectangleShape); + return CreateFixture(rectangleShape, collisionCategory, collidesWith); } - public Fixture CreateRectangle(float width, float height, float density, float rotation, Vector2 offset) + public Fixture CreateRectangle(float width, float height, float density, float rotation, Vector2 offset, Category collisionCategory, Category collidesWith) { Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2, Vector2.Zero, rotation); rectangleVertices.Translate(ref offset); PolygonShape rectangleShape = new PolygonShape(rectangleVertices, density); - return CreateFixture(rectangleShape); + return CreateFixture(rectangleShape, collisionCategory, collidesWith); } - public Fixture CreateCircle(float radius, float density) + public Fixture CreateCircle(float radius, float density, Category collisionCategory, Category collidesWith) { if (radius <= 0) throw new ArgumentOutOfRangeException("radius", "Radius must be more than 0 meters"); CircleShape circleShape = new CircleShape(radius, density); - return CreateFixture(circleShape); + return CreateFixture(circleShape, collisionCategory, collidesWith); } - public Fixture CreateCircle(float radius, float density, Vector2 offset) + public Fixture CreateCircle(float radius, float density, Vector2 offset, Category collisionCategory, Category collidesWith) { if (radius <= 0) throw new ArgumentOutOfRangeException("radius", "Radius must be more than 0 meters"); CircleShape circleShape = new CircleShape(radius, density); circleShape.Position = offset; - return CreateFixture(circleShape); + return CreateFixture(circleShape, collisionCategory, collidesWith); } - public Fixture CreatePolygon(Vertices vertices, float density) + public Fixture CreatePolygon(Vertices vertices, float density, Category collisionCategory, Category collidesWith) { if (vertices.Count <= 1) throw new ArgumentOutOfRangeException("vertices", "Too few points to be a polygon"); PolygonShape polygon = new PolygonShape(vertices, density); - return CreateFixture(polygon); + return CreateFixture(polygon, collisionCategory, collidesWith); } - public Fixture CreateEllipse(float xRadius, float yRadius, int edges, float density) + public Fixture CreateEllipse(float xRadius, float yRadius, int edges, float density, Category collisionCategory, Category collidesWith) { if (xRadius <= 0) throw new ArgumentOutOfRangeException("xRadius", "X-radius must be more than 0"); @@ -106,10 +106,10 @@ namespace FarseerPhysics.Dynamics Vertices ellipseVertices = PolygonTools.CreateEllipse(xRadius, yRadius, edges); PolygonShape polygonShape = new PolygonShape(ellipseVertices, density); - return CreateFixture(polygonShape); + return CreateFixture(polygonShape, collisionCategory, collidesWith); } - public List CreateCompoundPolygon(List list, float density) + public List CreateCompoundPolygon(List list, float density, Category collisionCategory, Category collidesWith) { List res = new List(list.Count); @@ -119,26 +119,26 @@ namespace FarseerPhysics.Dynamics if (vertices.Count == 2) { EdgeShape shape = new EdgeShape(vertices[0], vertices[1]); - res.Add(CreateFixture(shape)); + res.Add(CreateFixture(shape, collisionCategory, collidesWith)); } else { PolygonShape shape = new PolygonShape(vertices, density); - res.Add(CreateFixture(shape)); + res.Add(CreateFixture(shape, collisionCategory, collidesWith)); } } return res; } - public Fixture CreateLineArc(float radians, int sides, float radius, bool closed) + public Fixture CreateLineArc(float radians, int sides, float radius, bool closed, Category collisionCategory, Category collidesWith) { Vertices arc = PolygonTools.CreateArc(radians, sides, radius); arc.Rotate((MathHelper.Pi - radians) / 2); - return closed ? CreateLoopShape(arc) : CreateChainShape(arc); + return closed ? CreateLoopShape(arc, collisionCategory, collidesWith) : CreateChainShape(arc, collisionCategory, collidesWith); } - public List CreateSolidArc(float density, float radians, int sides, float radius) + public List CreateSolidArc(float density, float radians, int sides, float radius, Category collisionCategory, Category collidesWith) { Vertices arc = PolygonTools.CreateArc(radians, sides, radius); arc.Rotate((MathHelper.Pi - radians) / 2); @@ -148,7 +148,7 @@ namespace FarseerPhysics.Dynamics List triangles = Triangulate.ConvexPartition(arc, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(triangles, density); + return CreateCompoundPolygon(triangles, density, collisionCategory, collidesWith); } } } \ No newline at end of file diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs index 40381f0d4..d65d9a1b0 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs @@ -82,10 +82,10 @@ namespace FarseerPhysics.Dynamics /// public OnSeparationEventHandler OnSeparation; - internal Fixture() // Note: This is internal because it's used by Deserialization. + internal Fixture(Category collisionCategory, Category collidesWith) // Note: This is internal because it's used by Deserialization. { - _collisionCategories = Category.Cat1; - _collidesWith = Category.All; + _collisionCategories = collisionCategory; + _collidesWith = collidesWith; _collisionGroup = 0; //Fixture defaults @@ -93,7 +93,7 @@ namespace FarseerPhysics.Dynamics Restitution = 0f; } - public Fixture(Shape shape) : this() + public Fixture(Shape shape, Category collisionCategory, Category collidesWith) : this(collisionCategory, collidesWith) { Shape = shape.Clone(); @@ -375,15 +375,15 @@ namespace FarseerPhysics.Dynamics /// The cloned fixture. internal Fixture CloneOnto(Body body, Shape shape) { - Fixture fixture = new Fixture(shape.Clone()); - fixture.UserData = UserData; - fixture.Restitution = Restitution; - fixture.Friction = Friction; - fixture.IsSensor = IsSensor; - fixture._collisionGroup = _collisionGroup; - fixture._collisionCategories = _collisionCategories; - fixture._collidesWith = _collidesWith; - + Fixture fixture = new Fixture(shape.Clone(), _collisionCategories, _collidesWith) + { + UserData = UserData, + Restitution = Restitution, + Friction = Friction, + IsSensor = IsSensor, + _collisionGroup = _collisionGroup + }; + body.Add(fixture); return fixture; } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs index f3a778d24..b99ca20e6 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs @@ -17,43 +17,42 @@ namespace FarseerPhysics.Dynamics { public partial class World { - public virtual Body CreateBody(Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public virtual Body CreateBody(Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, bool findNewContacts = true) { - Body body = new Body(); - body.Position = position; - body.Rotation = rotation; - body.BodyType = bodyType; - - AddAsync(body); + Body body = new Body + { + Position = position, + Rotation = rotation, + BodyType = bodyType + }; + + AddAsync(body, findNewContacts); return body; } - public Body CreateEdge(Vector2 start, Vector2 end) + public Body CreateEdge(Vector2 start, Vector2 end, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(); - - body.CreateEdge(start, end); + Body body = CreateBody(bodyType: bodyType, findNewContacts: findNewContacts); + body.CreateEdge(start, end, collisionCategory, collidesWith); return body; } - public Body CreateChainShape(Vertices vertices, Vector2 position = new Vector2()) + public Body CreateChainShape(Vertices vertices, Vector2 position = new Vector2(), Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position); - - body.CreateChainShape(vertices); + Body body = CreateBody(position, findNewContacts: findNewContacts); + body.CreateChainShape(vertices, collisionCategory, collidesWith); return body; } - public Body CreateLoopShape(Vertices vertices, Vector2 position = new Vector2()) + public Body CreateLoopShape(Vertices vertices, Vector2 position = new Vector2(), Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position); - - body.CreateLoopShape(vertices); + Body body = CreateBody(position, findNewContacts: findNewContacts); + body.CreateLoopShape(vertices, collisionCategory, collidesWith); return body; } - public Body CreateRectangle(float width, float height, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateRectangle(float width, float height, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { if (width <= 0) throw new ArgumentOutOfRangeException("width", "Width must be more than 0 meters"); @@ -61,44 +60,44 @@ namespace FarseerPhysics.Dynamics if (height <= 0) throw new ArgumentOutOfRangeException("height", "Height must be more than 0 meters"); - Body body = CreateBody(position, rotation, bodyType); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2); - body.CreatePolygon(rectangleVertices, density); + body.CreatePolygon(rectangleVertices, density, collisionCategory, collidesWith); return body; } - public Body CreateCircle(float radius, float density, Vector2 position = new Vector2(), BodyType bodyType = BodyType.Static) + public Body CreateCircle(float radius, float density, Vector2 position = new Vector2(), BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, 0, bodyType); - body.CreateCircle(radius, density); + Body body = CreateBody(position, 0, bodyType, findNewContacts); + body.CreateCircle(radius, density, collisionCategory, collidesWith); return body; } - public Body CreateEllipse(float xRadius, float yRadius, int edges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateEllipse(float xRadius, float yRadius, int edges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, rotation, bodyType); - body.CreateEllipse(xRadius, yRadius, edges, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreateEllipse(xRadius, yRadius, edges, density, collisionCategory, collidesWith); return body; } - public Body CreatePolygon(Vertices vertices, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreatePolygon(Vertices vertices, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, rotation, bodyType); - body.CreatePolygon(vertices, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreatePolygon(vertices, density, collisionCategory, collidesWith); return body; } - public Body CreateCompoundPolygon(List list, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCompoundPolygon(List list, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //We create a single body - Body body = CreateBody(position, rotation, bodyType); - body.CreateCompoundPolygon(list, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreateCompoundPolygon(list, density, collisionCategory, collidesWith); return body; } - public Body CreateGear(float radius, int numberOfTeeth, float tipPercentage, float toothHeight, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateGear(float radius, int numberOfTeeth, float tipPercentage, float toothHeight, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Vertices gearPolygon = PolygonTools.CreateGear(radius, numberOfTeeth, tipPercentage, toothHeight); @@ -108,13 +107,13 @@ namespace FarseerPhysics.Dynamics //Decompose the gear: List list = Triangulate.ConvexPartition(gearPolygon, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(list, density, position, rotation, bodyType); + return CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith); } - return CreatePolygon(gearPolygon, density, position, rotation, bodyType); + return CreatePolygon(gearPolygon, density, position, rotation, bodyType, collisionCategory, collidesWith); } - public Body CreateCapsule(float height, float topRadius, int topEdges, float bottomRadius, int bottomEdges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsule(float height, float topRadius, int topEdges, float bottomRadius, int bottomEdges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { Vertices verts = PolygonTools.CreateCapsule(height, topRadius, topEdges, bottomRadius, bottomEdges); @@ -122,23 +121,25 @@ namespace FarseerPhysics.Dynamics if (verts.Count >= Settings.MaxPolygonVertices) { List vertList = Triangulate.ConvexPartition(verts, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(vertList, density, position, rotation, bodyType); + return CreateCompoundPolygon(vertList, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); } - return CreatePolygon(verts, density, position, rotation, bodyType); + return CreatePolygon(verts, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); } - public Body CreateCapsuleHorizontal(float width, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsuleHorizontal(float width, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //Create the middle rectangle Vertices rectangle = PolygonTools.CreateRectangle(width / 2, endRadius); - List list = new List(); - list.Add(rectangle); + List list = new List + { + rectangle + }; - Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType); - body.CreateCircle(endRadius, density, new Vector2(width / 2, 0)); - body.CreateCircle(endRadius, density, new Vector2(-width / 2, 0)); + Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); + body.CreateCircle(endRadius, density, new Vector2(width / 2, 0), collisionCategory, collidesWith); + body.CreateCircle(endRadius, density, new Vector2(-width / 2, 0), collisionCategory, collidesWith); //Create the two circles //CircleShape topCircle = new CircleShape(endRadius, density); @@ -150,17 +151,19 @@ namespace FarseerPhysics.Dynamics //body.CreateFixture(bottomCircle); return body; } - public Body CreateCapsule(float height, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsule(float height, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //Create the middle rectangle Vertices rectangle = PolygonTools.CreateRectangle(endRadius, height / 2); - List list = new List(); - list.Add(rectangle); + List list = new List() + { + rectangle + }; - Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType); - body.CreateCircle(endRadius, density, new Vector2(0, height / 2)); - body.CreateCircle(endRadius, density, new Vector2(0, -(height / 2))); + Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); + body.CreateCircle(endRadius, density, new Vector2(0, height / 2), collisionCategory, collidesWith); + body.CreateCircle(endRadius, density, new Vector2(0, -(height / 2)), collisionCategory, collidesWith); //Create the two circles //CircleShape topCircle = new CircleShape(endRadius, density); @@ -173,7 +176,7 @@ namespace FarseerPhysics.Dynamics return body; } - public Body CreateRoundedRectangle(float width, float height, float xRadius, float yRadius, int segments, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateRoundedRectangle(float width, float height, float xRadius, float yRadius, int segments, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Vertices verts = PolygonTools.CreateRoundedRectangle(width, height, xRadius, yRadius, segments); @@ -181,23 +184,23 @@ namespace FarseerPhysics.Dynamics if (verts.Count >= Settings.MaxPolygonVertices) { List vertList = Triangulate.ConvexPartition(verts, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(vertList, density, position, rotation, bodyType); + return CreateCompoundPolygon(vertList, density, position, rotation, bodyType, collisionCategory, collidesWith); } - return CreatePolygon(verts, density, position, rotation, bodyType); + return CreatePolygon(verts, density, position, rotation, bodyType, collisionCategory, collidesWith); } - public Body CreateLineArc(float radians, int sides, float radius, bool closed = false, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateLineArc(float radians, int sides, float radius, bool closed = false, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Body body = CreateBody(position, rotation, bodyType); - body.CreateLineArc(radians, sides, radius, closed); + body.CreateLineArc(radians, sides, radius, closed, collisionCategory, collidesWith); return body; } - public Body CreateSolidArc(float density, float radians, int sides, float radius, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateSolidArc(float density, float radians, int sides, float radius, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Body body = CreateBody(position, rotation, bodyType); - body.CreateSolidArc(density, radians, sides, radius); + body.CreateSolidArc(density, radians, sides, radius, collisionCategory, collidesWith); return body; } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs index e8b8cbea2..0aa2ef49a 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs @@ -936,7 +936,7 @@ namespace FarseerPhysics.Dynamics /// Warning: This method is locked during callbacks. /// /// Thrown when the world is Locked/Stepping. - public virtual void Add(Body body) + public virtual void Add(Body body, bool findNewContacts) { if (IsLocked) throw new WorldLockedException("Cannot add bodies when the World is locked."); @@ -972,8 +972,10 @@ namespace FarseerPhysics.Dynamics if (Enabled) body.CreateProxies(); - ContactManager.FindNewContacts(); - + if (findNewContacts) + { + ContactManager.FindNewContacts(); + } // Fire World events: @@ -1227,7 +1229,7 @@ namespace FarseerPhysics.Dynamics /// Add a rigid body. /// /// - public void AddAsync(Body body) + public void AddAsync(Body body, bool findNewContacts) { if (body == null) throw new ArgumentNullException("body"); @@ -1243,7 +1245,7 @@ namespace FarseerPhysics.Dynamics Debug.WriteLine("You are adding the same body more than once."); } else - Add(body); + Add(body, findNewContacts); } /// @@ -1322,7 +1324,7 @@ namespace FarseerPhysics.Dynamics if (_bodyAddList.Count > 0) { foreach (Body body in _bodyAddList) - Add(body); + Add(body, findNewContacts: true); _bodyAddList.Clear(); } @@ -1414,7 +1416,7 @@ namespace FarseerPhysics.Dynamics ProcessChanges(); if (Settings.EnableDiagnostics) - AddRemoveTime = TimeSpan.FromTicks(_watch.ElapsedTicks); + AddRemoveTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks); // If new fixtures were added, we need to find the new contacts. if (_worldHasNewFixture) @@ -1423,7 +1425,7 @@ namespace FarseerPhysics.Dynamics _worldHasNewFixture = false; } if (Settings.EnableDiagnostics) - NewContactsTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - AddRemoveTime; + NewContactsTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - AddRemoveTime; //FPE only: moved position and velocity iterations into Settings.cs TimeStep step; @@ -1443,12 +1445,12 @@ namespace FarseerPhysics.Dynamics ControllerList[i].Update(dt); } if (Settings.EnableDiagnostics) - ControllersUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime); + ControllersUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime); // Update contacts. This is where some contacts are destroyed. ContactManager.Collide(); if (Settings.EnableDiagnostics) - ContactsUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime); + ContactsUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime); // Integrate velocities, solve velocity constraints, and integrate positions. if (_stepComplete && step.dt > 0.0f) @@ -1456,7 +1458,7 @@ namespace FarseerPhysics.Dynamics Solve(ref step); } if (Settings.EnableDiagnostics) - SolveUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime); + SolveUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime); // Handle TOI events. if (Settings.ContinuousPhysics && step.dt > 0.0f) @@ -1464,7 +1466,7 @@ namespace FarseerPhysics.Dynamics SolveTOI(ref step, ref iterations); } if (Settings.EnableDiagnostics) - ContinuousPhysicsTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime + SolveUpdateTime); + ContinuousPhysicsTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime + SolveUpdateTime); if (step.dt > 0.0f) Fluid.Update(dt); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs index 6e6d04d58..5ec3a2b63 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs @@ -35,6 +35,7 @@ namespace Microsoft.Xna.Framework.Graphics format.GetGLFormat(GraphicsDevice, out glInternalFormat, out glFormat, out glType); Threading.BlockOnUIThread(() => { + var prev = GraphicsExtensions.GetBoundTexture2D(); GenerateGLTextureIfRequired(); int w = width; int h = height; @@ -80,6 +81,8 @@ namespace Microsoft.Xna.Framework.Graphics h = h / 2; ++level; } + + GL.BindTexture(TextureTarget.Texture2D, prev); }); } diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 073003f04..095528d41 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxServer", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.NetStandard", "Libraries\MonoGame.Framework\Src\MonoGame.Framework\MonoGame.Framework.Linux.NetStandard.csproj", "{33E95A21-E071-4432-819F-AA64CF3EF3F1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -181,6 +183,18 @@ Global {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|Any CPU.Build.0 = Release|Any CPU {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|x64.ActiveCfg = Unstable|x64 {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|x64.Build.0 = Unstable|x64 + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|x64.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|Any CPU.Build.0 = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|x64.ActiveCfg = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|x64.Build.0 = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -198,6 +212,7 @@ Global {D47E4AAA-C3E5-4F0D-B7FF-D3B54966DE51} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {2B0881F6-9C67-4446-A1F2-FC042763A462} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {33E95A21-E071-4432-819F-AA64CF3EF3F1} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/MacSolution.sln b/MacSolution.sln index 162365f9c..20e699038 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.MacOS.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.Posix", "Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj", "{F10CE3BB-26B8-446E-84D2-86D25E850F61}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,18 @@ Global {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|Any CPU.Build.0 = Release|Any CPU {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|x64.ActiveCfg = Release|Any CPU {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|x64.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|x64.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|Any CPU.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|x64.ActiveCfg = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|x64.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -195,6 +209,7 @@ Global {F17FB469-E9E6-4B1C-B887-4FE709D4D771} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} {35DDDA7D-328D-4A5D-BCBB-2E60C830A899} = {DE36F45F-F09E-4719-B953-00D148F7722A} {F10CE3BB-26B8-446E-84D2-86D25E850F61} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {20BC9336-B439-4BF1-8B65-D587DBF421D1} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 9c1bdeae7..515450184 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XNATypes", "Libraries\XNATy EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Libraries\SharpFont\Source\SharpFont\SharpFont.NetStandard.csproj", "{6911872D-40EF-400C-B0A1-9985A19ED488}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 @@ -115,6 +117,12 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|x64.Build.0 = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.ActiveCfg = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.Build.0 = Release|x64 + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.Build.0 = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.ActiveCfg = Release|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.Build.0 = Release|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,6 +140,7 @@ Global {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {1F318AC4-F808-4130-867F-B98DF9AA8F95} = {DE36F45F-F09E-4719-B953-00D148F7722A} {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A}