diff --git a/.gitattributes b/.gitattributes index 56f8e94c2..219869ee0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Declare files that will always have CRLF line endings on checkout. *.sln text eol=crlf *.cs text eol=crlf -*.xml text eol=crlf \ No newline at end of file +*.xml text eol=crlf +Barotrauma\BarotraumaServer\DedicatedServer.exe text eol=lf \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index 6f588373e..7adc4fb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -32,6 +32,8 @@ namespace Barotrauma public bool AllowInterrupt = false; public bool RemoveControlFromCharacter = true; + + public bool RunWhilePaused = true; public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null) { @@ -76,6 +78,8 @@ namespace Barotrauma #endif } + private float DeltaTime => CoroutineManager.Paused && !RunWhilePaused ? 0 : CoroutineManager.DeltaTime; + private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } @@ -169,12 +173,15 @@ namespace Barotrauma } if (LosFadeIn && clampedTimer / PanDuration > 0.8f) { - GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; + if (!GameMain.DevMode) + { + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; + } Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); - GameMain.LightManager.LosEnabled = true; } #endif - timer += CoroutineManager.DeltaTime; + timer += DeltaTime; yield return CoroutineStatus.Running; } @@ -184,7 +191,7 @@ namespace Barotrauma { cam.Translate(endPos - cam.Position); cam.Zoom = endZoom; - endTimer += CoroutineManager.DeltaTime; + endTimer += DeltaTime; yield return CoroutineStatus.Running; } @@ -192,8 +199,11 @@ namespace Barotrauma #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; - GameMain.LightManager.LosEnabled = true; - GameMain.LightManager.LosAlpha = 1f; + if (!GameMain.DevMode) + { + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + } #endif if (prevControlled != null && !prevControlled.Removed) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 6344495b1..6f3eda991 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -109,7 +109,7 @@ namespace Barotrauma } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove ? 0.01f : 0.2f; + float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { if (distSqrd > 10.0f || !character.CanMove) @@ -147,6 +147,7 @@ namespace Barotrauma { MainLimb.PullJointWorldAnchorB = Collider.SimPosition; MainLimb.PullJointEnabled = true; + MainLimb.body.LinearVelocity = newVelocity; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 5a0f10683..0f86bcc9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -298,6 +298,21 @@ namespace Barotrauma { keys[i].SetState(); } + + if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + { + ResetInputIfPrimaryMouse(InputType.Use); + ResetInputIfPrimaryMouse(InputType.Shoot); + ResetInputIfPrimaryMouse(InputType.Select); + void ResetInputIfPrimaryMouse(InputType inputType) + { + if (GameSettings.CurrentConfig.KeyMap.Bindings[inputType].MouseButton == MouseButton.PrimaryMouse) + { + keys[(int)inputType].Reset(); + } + } + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -316,8 +331,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (NeedsAir && !IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) @@ -636,7 +650,7 @@ namespace Barotrauma return closestItem; } - private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) + private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = MaxHighlightDistance) { Character closestCharacter = null; @@ -646,7 +660,7 @@ namespace Barotrauma { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } - float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); + float dist = c.GetDistanceToClosestLimb(mouseSimPos); if (dist < closestDist || (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 49914c30f..4ada1f0c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -29,6 +29,10 @@ namespace Barotrauma public abstract float State { get; } + public abstract string NumberToDisplay { get; } + + public abstract Color Color { get; } + public BossProgressBar(LocalizedString label) { FadeTimer = BossHealthBarDuration; @@ -46,6 +50,7 @@ namespace Barotrauma { Color = GUIStyle.Red }; + CreateNumberText(TopHealthBar); SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { @@ -56,12 +61,28 @@ namespace Barotrauma { Color = GUIStyle.Red }; + CreateNumberText(SideHealthBar); TopContainer.Visible = SideContainer.Visible = false; TopContainer.CanBeFocused = false; TopContainer.Children.ForEach(c => c.CanBeFocused = false); SideContainer.CanBeFocused = false; SideContainer.Children.ForEach(c => c.CanBeFocused = false); + + void CreateNumberText(GUIComponent parent) + { + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform) + { AbsoluteOffset = new Point(2) }, + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDark) + { + TextGetter = () => NumberToDisplay + }; + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform), + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorBright) + { + TextGetter = () => NumberToDisplay + }; + } } public abstract bool IsDuplicate(BossProgressBar progressBar); @@ -77,6 +98,12 @@ namespace Barotrauma public override bool Interrupted => Character.Removed || !Character.Enabled; + public override Color Color => + Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + + public override string NumberToDisplay => string.Empty; + public BossHealthBar(Character character) : base(character.DisplayName) { Character = character; @@ -96,7 +123,13 @@ namespace Barotrauma public override bool Completed => Mission.State >= Mission.Prefab.MaxProgressState; - public override bool Interrupted => Mission.Failed; + public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission); + + public override Color Color => GUIStyle.Red; + + public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ? + $"{Mission.State}/{Mission.Prefab.MaxProgressState}" : + string.Empty; public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) { @@ -155,7 +188,7 @@ namespace Barotrauma GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI); - private static bool ShouldDrawInventory(Character character) + public static bool ShouldDrawInventory(Character character) { var controller = character.SelectedItem?.GetComponent(); @@ -656,7 +689,8 @@ namespace Barotrauma } } - Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; + float dist = Vector2.Distance(character.FocusedCharacter.DrawPosition, character.DrawPosition); + Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) / dist * Math.Min(dist, Character.MaxDragDistance); startPos = cam.WorldToScreen(startPos); string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; @@ -722,6 +756,11 @@ namespace Barotrauma AddBossProgressBar(new MissionProgressBar(mission)); } + public static void ClearBossHealthBars() + { + bossHealthBars.Clear(); + } + private static void AddBossProgressBar(BossProgressBar progressBar, float damage = 0.0f) { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; @@ -791,7 +830,7 @@ namespace Barotrauma { foreach (var component in container.GetAllChildren()) { - component.Color = new Color(component.Color, (byte)(alpha * 255)); + component.Color = new Color(bossHealthBar.Color, (byte)(alpha * 255)); if (component is GUITextBlock textBlock) { textBlock.TextColor = new Color(bossHealthBar.Completed ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 73c92f5d0..eb8cc5fb6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -555,6 +555,10 @@ namespace Barotrauma if (jobIdentifier > 0) { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); + if (jobPrefab == null) + { + throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); + } foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) { float skillLevel = inc.ReadSingle(); @@ -562,7 +566,6 @@ namespace Barotrauma } } - // TODO: animations CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { ID = infoID, @@ -573,10 +576,7 @@ namespace Barotrauma ch.Head.HairColor = hairColor; ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); - if (ch.Job != null) - { - ch.Job.OverrideSkills(skillLevels); - } + ch.Job?.OverrideSkills(skillLevels); ch.ExperiencePoints = inc.ReadUInt16(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 23db24f69..8139f283c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -593,6 +593,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.Faction = inc.ReadIdentifier(); character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d9801606c..57a4c8015 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -87,6 +87,8 @@ namespace Barotrauma /// Container for the icons above the health bar /// private GUIComponent afflictionIconContainer; + private float afflictionIconRefreshTimer; + const float AfflictionIconRefreshInterval = 1.0f; private GUIButton showHiddenAfflictionsButton; @@ -861,7 +863,7 @@ namespace Barotrauma { treatmentButton.ToolTip = RichString.Rich( - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + $"‖color:gui.green‖[{PlayerInput.PrimaryMouseLabel}] " + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + treatmentButton.ToolTip.NestedStr); } @@ -1157,15 +1159,20 @@ namespace Barotrauma } } - afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + afflictionIconRefreshTimer -= deltaTime; + if (afflictionIconRefreshTimer <= 0.0f) { - if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } - if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } - var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); - var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); - return index1.CompareTo(index2); - }); - (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + { + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconRefreshTimer = AfflictionIconRefreshInterval; + } Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) @@ -1983,6 +1990,7 @@ namespace Barotrauma { newAfflictions.Clear(); newPeriodicEffects.Clear(); + bool newAdded = false; byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -2062,6 +2070,7 @@ namespace Barotrauma { existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction, limb); + newAdded = true; } existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) @@ -2071,6 +2080,8 @@ namespace Barotrauma foreach (var periodicEffect in newPeriodicEffects) { if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } + if (existingAffliction.Strength < periodicEffect.effect.MinStrength) { continue; } + if (periodicEffect.effect.MaxStrength > 0 && existingAffliction.Strength > periodicEffect.effect.MaxStrength) { continue; } //timer has wrapped around, apply the effect if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { @@ -2086,6 +2097,11 @@ namespace Barotrauma CalculateVitality(); DisplayedVitality = Vitality; + + if (newAdded) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } partial void UpdateSkinTint() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs new file mode 100644 index 000000000..bcc7d4083 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs @@ -0,0 +1,22 @@ +#nullable enable + +using System; + +namespace Barotrauma +{ + internal static class HealingCooldown + { + public static float NormalizedCooldown => MathF.Min((float) (DateTimeOffset.UtcNow - OnCooldownUntil).TotalSeconds / CooldownDuration, 0f); + public static bool IsOnCooldown => DateTimeOffset.UtcNow < OnCooldownUntil; + + private static DateTimeOffset OnCooldownUntil = DateTimeOffset.MinValue; + private const float CooldownDuration = 0.5f; + + public static readonly Identifier MedicalItemTag = new Identifier("medical"); + + public static void PutOnCooldown() + { + OnCooldownUntil = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index bfe5af4d3..913f15dca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Barotrauma.IO; +using Barotrauma.Utils; using System.Linq; using System.Xml.Linq; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; @@ -260,9 +261,7 @@ namespace Barotrauma { if (enableHuskSprite) { - List otherWearablesWithHusk = new List() { HuskSprite }; - otherWearablesWithHusk.AddRange(OtherWearables); - OtherWearables = otherWearablesWithHusk; + OtherWearables.Insert(0, HuskSprite); UpdateWearableTypesToHide(); } else @@ -546,7 +545,7 @@ namespace Barotrauma { foreach (var affliction in result.Afflictions) { - if (affliction is AfflictionBleeding) + if (affliction is AfflictionBleeding bleeding && bleeding.Prefab.DamageParticles) { bleedingDamage += affliction.GetVitalityDecrease(null); } @@ -555,7 +554,7 @@ namespace Barotrauma float damage = 0; foreach (var affliction in result.Afflictions) { - if (affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { damage += affliction.GetVitalityDecrease(null); } @@ -564,11 +563,11 @@ namespace Barotrauma float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { - if (damageModifier.MatchesAfflictionType("damage")) + if (damageModifier.MatchesAfflictionType(AfflictionPrefab.DamageType)) { damageMultiplier *= damageModifier.DamageMultiplier; } - else if (damageModifier.MatchesAfflictionType("bleeding")) + else if (damageModifier.MatchesAfflictionType(AfflictionPrefab.BleedingType)) { bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } @@ -728,11 +727,11 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || - wearingItems.Any(w => w != null && w.HideLimb); + WearingItems.Any(w => w.HideLimb); bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); @@ -828,7 +827,7 @@ namespace Barotrauma LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } float step = depthStep; - WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); + WearableSprite onlyDrawable = WearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) { spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; @@ -965,31 +964,28 @@ namespace Barotrauma public void UpdateWearableTypesToHide() { + alphaClipEffectParams?.Clear(); + wearableTypeHidingSprites.Clear(); - if (WearingItems != null && WearingItems.Count > 0) + + void addWearablesFrom(IReadOnlyList wearableSprites) { + if (wearableSprites.Count <= 0) { return; } + wearableTypeHidingSprites.AddRange( - WearingItems.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0)); - } - if (OtherWearables != null && OtherWearables.Count > 0) - { - wearableTypeHidingSprites.AddRange( - OtherWearables.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0)); + wearableSprites.Where(w => w.HideWearablesOfType.Count > 0)); } + addWearablesFrom(WearingItems); + addWearablesFrom(OtherWearables); + wearableTypesToHide.Clear(); - if (wearableTypeHidingSprites.Count > 0) + + if (wearableTypeHidingSprites.Count <= 0) { return; } + + foreach (WearableSprite sprite in wearableTypeHidingSprites) { - foreach (WearableSprite sprite in wearableTypeHidingSprites) - { - foreach (WearableType type in sprite.HideWearablesOfType) - { - if (!wearableTypesToHide.Contains(type)) - { - wearableTypesToHide.Add(type); - } - } - } + wearableTypesToHide.UnionWith(sprite.HideWearablesOfType); } } @@ -1071,7 +1067,13 @@ namespace Barotrauma } } - private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) + private ( + Color FinalColor, + Vector2 Origin, + float Rotation, + float Scale, + float Depth) + CalculateDrawParameters(WearableSprite wearable, float depthStep, Color color, float alpha) { var sprite = ActiveSprite; if (wearable.InheritSourceRect) @@ -1163,27 +1165,118 @@ namespace Barotrauma float finalAlpha = alpha * wearableColor.A; Color finalColor = color.Multiply(wearableColor); finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); - wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); + + return (finalColor, origin, rotation, scale, depth); } - private WearableSprite GetWearableSprite(WearableType type)//, bool random = false) + private static Effect alphaClipEffect; + private Dictionary> alphaClipEffectParams; + private void ApplyAlphaClip(SpriteBatch spriteBatch, WearableSprite wearable, WearableSprite alphaClipper, SpriteEffects spriteEffect) + { + SpriteRecorder.Command makeCommand(WearableSprite w) + { + var (_, origin, rotation, scale, _) + = CalculateDrawParameters(w, 0f, Color.White, 0f); + + var command = SpriteRecorder.Command.FromTransform( + texture: w.Sprite.Texture, + pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), + srcRect: w.Sprite.SourceRect, + color: Color.White, + rotation: rotation, + origin: origin, + scale: new Vector2(scale, scale), + effects: spriteEffect, + depth: 0f, + index: 0); + + return command; + } + + void spacesFromCommand(WearableSprite w, SpriteRecorder.Command command, out CoordinateSpace2D textureSpace, out CoordinateSpace2D worldSpace) + { + var (topLeft, bottomLeft, topRight) = spriteEffect switch + { + SpriteEffects.None + => (command.VertexTL, command.VertexBL, command.VertexTR), + SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically + => (command.VertexBR, command.VertexTR, command.VertexBL), + SpriteEffects.FlipHorizontally + => (command.VertexTR, command.VertexBR, command.VertexTL), + SpriteEffects.FlipVertically + => (command.VertexBL, command.VertexTL, command.VertexBR) + }; + + textureSpace = new CoordinateSpace2D + { + Origin = topLeft.TextureCoordinate, + I = topRight.TextureCoordinate - topLeft.TextureCoordinate, + J = bottomLeft.TextureCoordinate - topLeft.TextureCoordinate + }; + + worldSpace = new CoordinateSpace2D + { + Origin = topLeft.Position.DiscardZ(), + I = topRight.Position.DiscardZ() - topLeft.Position.DiscardZ(), + J = bottomLeft.Position.DiscardZ() - topLeft.Position.DiscardZ() + }; + } + + var wearableCommand = makeCommand(wearable); + var clipperCommand = makeCommand(alphaClipper); + + spacesFromCommand(wearable, wearableCommand, out var wearableTextureSpace, out var wearableWorldSpace); + spacesFromCommand(alphaClipper, clipperCommand, out var clipperTextureSpace, out var clipperWorldSpace); + + var wearableUvToClipperUv = + wearableTextureSpace.CanonicalToLocal + * wearableWorldSpace.LocalToCanonical + * clipperWorldSpace.CanonicalToLocal + * clipperTextureSpace.LocalToCanonical; + + alphaClipEffect ??= EffectLoader.Load("Effects/wearableclip"); + alphaClipEffectParams ??= new Dictionary>(); + if (!alphaClipEffectParams.ContainsKey(wearable)) { alphaClipEffectParams.Add(wearable, new Dictionary()); } + + var paramsToPass = new SpriteBatch.EffectWithParams + { + Effect = alphaClipEffect, + Params = alphaClipEffectParams[wearable] + }; + + paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv; + paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width; + paramsToPass.Params["aCutoff"] = 2f / 255f; + paramsToPass.Params["xTexture"] = wearable.Sprite.Texture; + paramsToPass.Params["xStencil"] = alphaClipper.Sprite.Texture; + spriteBatch.SwapEffect(paramsToPass); + } + + private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) + { + var (finalColor, origin, rotation, scale, depth) + = CalculateDrawParameters(wearable, depthStep, color, alpha); + + var prevEffect = spriteBatch.GetCurrentEffect(); + var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables); + bool shouldApplyAlphaClip = alphaClipper != null && wearable != alphaClipper; + if (shouldApplyAlphaClip) + { + ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect); + } + wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); + if (shouldApplyAlphaClip) + { + spriteBatch.SwapEffect(effect: prevEffect); + } + } + + private WearableSprite GetWearableSprite(WearableType type) { var info = character.Info; if (info == null) { return null; } - ContentXElement element; - /*if (random) - { - element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet)?.GetRandom(Rand.RandSync.ClientOnly); - } - else - {*/ - element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); - //} - if (element != null) - { - return new WearableSprite(element.GetChildElement("sprite"), type); - } - return null; + ContentXElement element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); + return element != null ? new WearableSprite(element.GetChildElement("sprite"), type) : null; } partial void RemoveProjSpecific() @@ -1206,8 +1299,8 @@ namespace Barotrauma LightSource?.Remove(); LightSource = null; - OtherWearables?.ForEach(w => w.Sprite.Remove()); - OtherWearables = null; + OtherWearables.ForEach(w => w.Sprite.Remove()); + OtherWearables.Clear(); HuskSprite?.Sprite.Remove(); HuskSprite = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 85877787b..76afc54f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -92,7 +92,7 @@ namespace Barotrauma public Option UgcId = Option.None(); - public Option InstallTime = Option.None(); + public Option InstallTime = Option.None(); public bool HasFile(File file) => Files.Any(f => @@ -120,7 +120,7 @@ namespace Barotrauma public void DiscardHashAndInstallTime() { ExpectedHash = null; - InstallTime = Option.None(); + InstallTime = Option.None(); } public static string IncrementModVersion(string modVersion) @@ -159,8 +159,8 @@ namespace Barotrauma addRootAttribute("gameversion", GameMain.Version); if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } - if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); } - + if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", installTime); } + files.ForEach(f => rootElement.Add(f.ToXElement())); doc.Add(rootElement); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 92ce38770..95e5c377a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -49,7 +49,7 @@ namespace Barotrauma && ugcId is SteamWorkshopId workshopId && item.Id == workshopId.Value && p.InstallTime.TryUnwrap(out var installTime) - && item.LatestUpdateTime <= installTime)) + && item.LatestUpdateTime <= installTime.ToUtcValue())) .ToArray(); if (!needInstalling.Any()) { return Enumerable.Empty(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index b5ca45799..728d7233b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -639,7 +639,7 @@ namespace Barotrauma { if (Submarine.MainSub == null) { return; } MapEntity.SelectedList.Clear(); - MapEntity.mapEntityList.ForEach(me => me.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); WikiImage.Create(Submarine.MainSub); })); @@ -752,7 +752,7 @@ namespace Barotrauma state = !GameMain.LightManager.LosEnabled; } GameMain.LightManager.LosEnabled = state; - NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("los", false); @@ -763,7 +763,7 @@ namespace Barotrauma state = !GameMain.LightManager.LightingEnabled; } GameMain.LightManager.LightingEnabled = state; - NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("lighting|lights", false); @@ -781,7 +781,7 @@ namespace Barotrauma hull.OriginalAmbientLight = null; } } - NewMessage("Restored all hull ambient lights", Color.White); + NewMessage("Restored all hull ambient lights", Color.Yellow); return; } @@ -803,11 +803,11 @@ namespace Barotrauma if (add) { - NewMessage($"Set ambient light color to {color}.", Color.White); + NewMessage($"Set ambient light color to {color}.", Color.Yellow); } else { - NewMessage($"Increased ambient light by {color}.", Color.White); + NewMessage($"Increased ambient light by {color}.", Color.Yellow); } }); AssignRelayToServer("ambientlight", false); @@ -1124,10 +1124,32 @@ namespace Barotrauma state = !GameMain.DebugDraw; } GameMain.DebugDraw = state; - NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("devmode", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.DevMode; + } + GameMain.DevMode = state; + if (GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = false; + GameMain.LightManager.LosEnabled = false; + } + else + { + GameMain.LightManager.LightingEnabled = true; + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + } + NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); + }); + AssignRelayToServer("devmode", false); + AssignOnExecute("debugdrawlocalization", (string[] args) => { if (args.None() || !bool.TryParse(args[0], out bool state)) @@ -1135,7 +1157,7 @@ namespace Barotrauma state = !TextManager.DebugDraw; } TextManager.DebugDraw = state; - NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1148,19 +1170,19 @@ namespace Barotrauma var config = GameSettings.CurrentConfig; config.Audio.DisableVoiceChatFilters = state; GameSettings.SetCurrentConfig(config); - NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.Yellow); }); AssignRelayToServer("togglevoicechatfilters", false); commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) => { GameMain.ShowFPS = !GameMain.ShowFPS; - NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("showperf", "showperf: Toggle performance statistics on/off.", (string[] args) => { GameMain.ShowPerf = !GameMain.ShowPerf; - NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.White); + NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.Yellow); })); AssignOnClientExecute("netstats", (string[] args) => @@ -1172,55 +1194,55 @@ namespace Barotrauma commands.Add(new Command("hudlayoutdebugdraw|debugdrawhudlayout", "hudlayoutdebugdraw: Toggle the debug drawing mode of HUD layout areas on/off.", (string[] args) => { HUDLayoutSettings.DebugDraw = !HUDLayoutSettings.DebugDraw; - NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("interactdebugdraw|debugdrawinteract", "interactdebugdraw: Toggle the debug drawing mode of item interaction ranges on/off.", (string[] args) => { Character.DebugDrawInteract = !Character.DebugDrawInteract; - NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.White); + NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.Yellow); }, isCheat: true)); AssignOnExecute("togglehud|hud", (string[] args) => { GUI.DisableHUD = !GUI.DisableHUD; GameMain.Instance.IsMouseVisible = !GameMain.Instance.IsMouseVisible; - NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White); + NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.Yellow); }); AssignRelayToServer("togglehud|hud", false); AssignOnExecute("toggleupperhud", (string[] args) => { GUI.DisableUpperHUD = !GUI.DisableUpperHUD; - NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.White); + NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.Yellow); }); AssignRelayToServer("toggleupperhud", false); AssignOnExecute("toggleitemhighlights", (string[] args) => { GUI.DisableItemHighlights = !GUI.DisableItemHighlights; - NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.White); + NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.Yellow); }); AssignRelayToServer("toggleitemhighlights", false); AssignOnExecute("togglecharacternames", (string[] args) => { GUI.DisableCharacterNames = !GUI.DisableCharacterNames; - NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.White); + NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.Yellow); }); AssignRelayToServer("togglecharacternames", false); AssignOnExecute("followsub", (string[] args) => { Camera.FollowSub = !Camera.FollowSub; - NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White); + NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.Yellow); }); AssignRelayToServer("followsub", false); AssignOnExecute("toggleaitargets|aitargets", (string[] args) => { AITarget.ShowAITargets = !AITarget.ShowAITargets; - NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.White); + NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.Yellow); }); AssignRelayToServer("toggleaitargets|aitargets", false); @@ -1229,21 +1251,49 @@ namespace Barotrauma HumanAIController.debugai = !HumanAIController.debugai; if (HumanAIController.debugai) { + GameMain.DevMode = true; GameMain.DebugDraw = true; GameMain.LightManager.LightingEnabled = false; GameMain.LightManager.LosEnabled = false; } else { + GameMain.DevMode = false; GameMain.DebugDraw = false; GameMain.LightManager.LightingEnabled = true; GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); + NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); + AssignOnExecute("showmonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = true; + CreatureMetrics.Save(); + NewMessage("All monsters are now visible in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("showmonsters", false); + + AssignOnExecute("hidemonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = false; + CreatureMetrics.Save(); + NewMessage("All monsters that haven't yet been encountered in the game are now hidden in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("hidemonsters", false); + AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs index ce251c05a..55f82a1d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -8,7 +8,8 @@ partial class CheckObjectiveAction : BinaryOptionAction public enum CheckType { Added, - Completed + Completed, + Incomplete } [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] @@ -30,8 +31,13 @@ partial class CheckObjectiveAction : BinaryOptionAction { CheckType.Added => true, CheckType.Completed => segment.IsCompleted, + CheckType.Incomplete => !segment.IsCompleted, _ => false }; } + else if (Type == CheckType.Incomplete) + { + success = true; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs index 809bdb393..5e4415bd6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma; @@ -13,11 +14,22 @@ partial class UIHighlightAction : EventAction bool useCircularFlash = false; if (Id != ElementId.None) { - FindAndFlashComponents(c => Equals(Id, c.UserData)); + var predicate = (GUIComponent c) => c is not null && Equals(Id, c.UserData); + if (!FindAndFlashAddedComponents(predicate)) + { + if (predicate(GUIMessageBox.VisibleBox)) + { + Flash(GUIMessageBox.VisibleBox); + } + else + { + FindAndFlashMessageBoxComponents(predicate); + } + } } else if (!EntityIdentifier.IsEmpty) { - FindAndFlashComponents(c => + FindAndFlashAddedComponents(c => c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); } else if (!OrderIdentifier.IsEmpty) @@ -26,26 +38,26 @@ partial class UIHighlightAction : EventAction bool foundMinimapNode = false; if (!OrderTargetTag.IsEmpty) { - foundMinimapNode = FindAndFlashComponents(c => + foundMinimapNode = FindAndFlashAddedComponents(c => c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); } if (!foundMinimapNode) { - FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, + FindAndFlashAddedComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, c => c.UserData is Order order && order.Identifier == OrderIdentifier, c => Equals(OrderCategory, c.UserData)); } } - bool FindAndFlashComponents(params Func[] predicates) + bool FindAndFlashComponents(IEnumerable components, params Func[] predicates) { foreach (var predicate in predicates) { if (HighlightMultiple) { bool found = false; - foreach (var component in GUI.GetAdditions()) + foreach (var component in components) { if (predicate(component)) { @@ -55,7 +67,7 @@ partial class UIHighlightAction : EventAction }; return found; } - else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component) + else if (components.FirstOrDefault(predicate) is GUIComponent component) { Flash(component); return true; @@ -64,6 +76,10 @@ partial class UIHighlightAction : EventAction return false; } + bool FindAndFlashAddedComponents(params Func[] predicates) => FindAndFlashComponents(GUI.GetAdditions(), predicates); + + bool FindAndFlashMessageBoxComponents(params Func[] predicates) => FindAndFlashComponents(GUIMessageBox.VisibleBox?.GetAllChildren() ?? Enumerable.Empty(), predicates); + void Flash(GUIComponent component) { if (component.FlashTimer <= 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 9207f0ddd..ef02adad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -661,18 +661,36 @@ namespace Barotrauma case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); int locationIndex = msg.ReadInt32(); + int destinationIndex = msg.ReadInt32(); + string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; - if (GameMain.GameSession?.Map is { } map && locationIndex > 0 && locationIndex < map.Locations.Count) + if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) { - map.Discover(map.Locations[locationIndex], checkTalents: false); + Location location = map.Locations[locationIndex]; + map.Discover(location, checkTalents: false); + + LocationConnection? connection = null; + if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) + { + Location destination = map.Locations[destinationIndex]; + connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); + } + if (connection != null) + { + location.UnlockMission(prefab, connection); + } + else + { + location.UnlockMission(prefab); + } } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 08356c60c..1a9941f42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -10,8 +10,7 @@ namespace Barotrauma public override RichString GetMissionRewardText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - + LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; if (rewardPerCrate.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs index fdfc6dfdb..d4b9a8dec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -21,6 +21,7 @@ namespace Barotrauma { new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 8, fadeOut: false, startZoom: 1.0f, endZoom: 0.3f * GUI.yScale) { + RunWhilePaused = false, EndWaitDuration = 3.0f }; } @@ -40,6 +41,7 @@ namespace Barotrauma { new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) { + RunWhilePaused = false, EndWaitDuration = float.PositiveInfinity }; }, delay: 3.0f); @@ -52,6 +54,7 @@ namespace Barotrauma { new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 5.0f, fadeOut: false, losFadeIn: false, startZoom: 1.0f, endZoom: 0.4f * GUI.yScale) { + RunWhilePaused = false, EndWaitDuration = cameraWaitDuration }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 410c7ec87..4a0e51172 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -32,10 +32,28 @@ namespace Barotrauma return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } + /// + /// Returns the amount of marks you get from the reward (e.g. "3,000 mk") + /// + protected LocalizedString GetRewardAmountText(Submarine sub) + { + int baseReward = GetReward(sub); + int finalReward = GetFinalReward(sub); + string rewardAmountText = string.Format(CultureInfo.InvariantCulture, "{0:N0}", baseReward); + if (finalReward > baseReward) + { + rewardAmountText += $" + {string.Format(CultureInfo.InvariantCulture, "{0:N0}", finalReward - baseReward)}"; + } + return TextManager.GetWithVariable("currencyformat", "[credits]", rewardAmountText); + } + + /// + /// Returns the full reward text of the mission (e.g. "Reward: 2,000 mk" or "Reward: 500 mk x 2 (out of max 5) = 1,000 mk") + /// public virtual RichString GetMissionRewardText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); + LocalizedString rewardText = GetRewardAmountText(sub); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖")); } public RichString GetReputationRewardText() @@ -43,27 +61,30 @@ namespace Barotrauma List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - FactionPrefab targetFaction; + FactionPrefab targetFactionPrefab; if (reputationReward.Key == "location" ) { - targetFaction = OriginLocation.Faction?.Prefab; + targetFactionPrefab = OriginLocation.Faction?.Prefab; } else { - FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFaction); + FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); + } + + if (targetFactionPrefab == null) + { + return string.Empty; } - LocalizedString name; - if (targetFaction != null) + float totalReputationChange = reputationReward.Value; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) { - name = $"‖color:{XMLExtensions.ToStringHex(targetFaction.IconColor)}‖{targetFaction.Name}‖end‖"; + totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); } - else - { - name = TextManager.Get(reputationReward.Key); - } - float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); - string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers + + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); + string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index ac2123cc1..5af8c6e02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -516,7 +516,7 @@ namespace Barotrauma new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) { Enabled = CanHire(characterInfo), - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), UserData = characterInfo, OnClicked = CreateRenamingComponent }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 5074af0aa..9223c03b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -106,6 +106,11 @@ namespace Barotrauma public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + /// + /// A horizontal scaling factor for low aspect ratios (small width relative to height) + /// + public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; public static int UIWidth @@ -2586,8 +2591,11 @@ namespace Barotrauma public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { - if (messages.Any(msg => msg.Text == message)) { return; } - messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); + lock (mutex) + { + if (messages.Any(msg => msg.Text == message)) { return; } + messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); + } if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2597,34 +2605,37 @@ namespace Barotrauma var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } - bool overlapFound = true; - int tries = 0; - while (overlapFound) - { - overlapFound = false; - foreach (var otherMessage in messages) - { - float xDiff = otherMessage.Pos.X - newMessage.Pos.X; - if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } - float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; - if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } - Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); - if (moveDir.LengthSquared() > 0.0001f) - { - moveDir = Vector2.Normalize(moveDir); - } - else - { - moveDir = Rand.Vector(1.0f); - } - moveDir.Y = -Math.Abs(moveDir.Y); - newMessage.Pos -= Vector2.UnitY * 10; - } - tries++; - if (tries > 20) { break; } - } - messages.Add(newMessage); + lock (mutex) + { + bool overlapFound = true; + int tries = 0; + while (overlapFound) + { + overlapFound = false; + foreach (var otherMessage in messages) + { + float xDiff = otherMessage.Pos.X - newMessage.Pos.X; + if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } + float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; + if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } + Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); + if (moveDir.LengthSquared() > 0.0001f) + { + moveDir = Vector2.Normalize(moveDir); + } + else + { + moveDir = Rand.Vector(1.0f); + } + moveDir.Y = -Math.Abs(moveDir.Y); + newMessage.Pos -= Vector2.UnitY * 10; + } + tries++; + if (tries > 20) { break; } + } + messages.Add(newMessage); + } } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index d27e2c086..10334efc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,9 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - private static readonly object mutex = new object(); - - protected GUICanvas() : base(size, parent: null) { } + protected GUICanvas() : base(Size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -33,7 +31,7 @@ namespace Barotrauma //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected private readonly List> childrenWeakRef = new List>(); - private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + private static Vector2 Size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); protected override Rectangle NonScaledUIRect => UIRect; @@ -41,25 +39,27 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - lock (mutex) + CrossThread.RequestExecutionOnMainThread(RefreshChildren); + } + + private static void RefreshChildren() + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) - { - _instance.childrenWeakRef.Add(new WeakReference(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--) + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 1; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) - { - _instance.childrenWeakRef.RemoveAt(i); - } + _instance.childrenWeakRef.RemoveAt(i); } } } @@ -67,7 +67,7 @@ namespace Barotrauma // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Vector2 recalculatedSize = size; + Vector2 recalculatedSize = Size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well for (int i = 0; i < Instance.childrenWeakRef.Count; i++) @@ -109,7 +109,7 @@ namespace Barotrauma } } - Instance.Resize(size, resizeChildren: true); + Instance.Resize(Size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); _instance.children.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 96a646db8..b6460f83f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1143,14 +1143,13 @@ namespace Barotrauma bool wrap = element.GetAttributeBool("wrap", true); Alignment alignment = element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center); - GUIFont font; - if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out font)) + if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out GUIFont font)) { font = GUIStyle.Font; } var textBlock = new GUITextBlock(RectTransform.Load(element, parent), - text, color, font, alignment, wrap: wrap, style: style) + RichString.Rich(text), color, font, alignment, wrap: wrap, style: style) { TextScale = scale }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index dd848bd65..8a795ccd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -244,18 +244,16 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) + public GUIComponent AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null, Color? color = null, Color? textColor = null) { toolTip ??= ""; if (selectMultiple) { - var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, style: "ListBoxElement") + var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color) { UserData = userData, ToolTip = toolTip }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) { UserData = userData, @@ -275,7 +273,7 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (tickBox.Selected) + if (tickBox is { Selected: true }) { selectedDataMultiple.Add(child.UserData); selectedIndexMultiple.Add(i); @@ -289,11 +287,11 @@ namespace Barotrauma return true; } }; + return frame; } else { - new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, text, style: "ListBoxElement") + return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor) { UserData = userData, ToolTip = toolTip @@ -323,7 +321,7 @@ namespace Barotrauma } else { - if (!(component is GUITextBlock textBlock)) + if (component is not GUITextBlock textBlock) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index b03bc27a6..aa1a6a882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1059,6 +1059,7 @@ namespace Barotrauma GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } + if (!child.Enabled) { return; } bool wasSelected = true; if (OnSelected != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index f14c4e1c5..4d3f69ea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -236,7 +236,8 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), style: "UIToggleButton") { - OnClicked = Close + OnClicked = Close, + UserData = UIHighlightAction.ElementId.MessageBoxCloseButton } }; InputType? closeInput = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 4f938a89f..9edfae736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -313,7 +313,9 @@ namespace Barotrauma break; } - RectTransform.MinSize = TextBox.RectTransform.MinSize; + RectTransform.MinSize = new Point( + Math.Max(rectT.MinSize.X, TextBox.RectTransform.MinSize.X), + Math.Max(rectT.MinSize.Y, TextBox.RectTransform.MinSize.Y)); LayoutGroup.Recalculate(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 07e976ada..e3ad34ad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -140,6 +140,7 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); + public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); public static Point ItemFrameMargin { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 4972e95d3..1ae121338 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -438,7 +438,7 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + if ((PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked()) && selected) { if (!mouseHeldInside) { Deselect(); } mouseHeldInside = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index dec3485bb..98921a51b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -143,14 +143,13 @@ namespace Barotrauma } 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); - HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom + ButtonAreaTop.Height, messageAreaWidth, ButtonAreaTop.Height); - bool isFourByThree = GUI.IsFourByThree(); - int chatBoxWidth = !isFourByThree ? (int)(475 * GUI.Scale) : (int)(375 * GUI.Scale); + int chatBoxWidth = (int)(475 * GUI.Scale * GUI.AspectRatioAdjustment); int chatBoxHeight = (int)Math.Max(GameMain.GraphicsHeight * 0.25f, 150); ChatBoxArea = new Rectangle(Padding, GameMain.GraphicsHeight - Padding - chatBoxHeight, chatBoxWidth, chatBoxHeight); @@ -187,19 +186,26 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { - DrawRectangle(ButtonAreaTop, Color.White * 0.5f); - DrawRectangle(TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); - DrawRectangle(MessageAreaTop, GUIStyle.Orange * 0.5f); - DrawRectangle(CrewArea, Color.Blue * 0.5f); - DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); - DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); - DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); - DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); - DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); - DrawRectangle(ItemHUDArea, Color.Magenta * 0.3f); + DrawRectangle(nameof(ButtonAreaTop), ButtonAreaTop, Color.White * 0.5f); + DrawRectangle(nameof(TutorialObjectiveListArea), TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); + DrawRectangle(nameof(MessageAreaTop), MessageAreaTop, GUIStyle.Orange * 0.5f); + DrawRectangle(nameof(CrewArea), CrewArea, Color.Blue * 0.5f); + DrawRectangle(nameof(ChatBoxArea), ChatBoxArea, Color.Cyan * 0.5f); + DrawRectangle(nameof(HealthBarArea), HealthBarArea, Color.Red * 0.5f); + DrawRectangle(nameof(HealthBarAfflictionArea), HealthBarAfflictionArea, Color.Red * 0.5f); + DrawRectangle(nameof(InventoryAreaLower), InventoryAreaLower, Color.Yellow * 0.5f); + DrawRectangle(nameof(HealthWindowAreaLeft), HealthWindowAreaLeft, Color.Red * 0.5f); + DrawRectangle(nameof(BottomRightInfoArea), BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(nameof(ItemHUDArea), ItemHUDArea, Color.Magenta * 0.3f); - void DrawRectangle(Rectangle r, Color c) => GUI.DrawRectangle(spriteBatch, r, c); + void DrawRectangle(string label, Rectangle r, Color c) + { + if (!label.IsNullOrEmpty()) + { + GUI.DrawString(spriteBatch, r.Location.ToVector2() + Vector2.One * 3, label, c, font: GUIStyle.SmallFont); + } + GUI.DrawRectangle(spriteBatch, r, c); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 6f5272743..81598fdf2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -143,33 +143,32 @@ namespace Barotrauma { public readonly MedicalClinic.NetAffliction Target; public readonly ImmutableArray ElementsToDisable; + public readonly GUIComponent TargetElement; - public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + public PopupAffliction(ImmutableArray elementsToDisable, GUIComponent component, MedicalClinic.NetAffliction target) { Target = target; ElementsToDisable = elementsToDisable; + TargetElement = component; } } private readonly struct PopupAfflictionList { public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIListBox ListElement; public readonly GUIButton TreatAllButton; - public readonly List Afflictions; + public readonly HashSet Afflictions; - public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIListBox listElement, GUIButton treatAllButton) { + ListElement = listElement; Target = crewMember; TreatAllButton = treatAllButton; - Afflictions = new List(); + Afflictions = new HashSet(); } } - // private enum SortMode - // { - // Severity - // } - private readonly MedicalClinic medicalClinic; private readonly GUIComponent container; private Point prevResolution; @@ -221,23 +220,22 @@ namespace Barotrauma private void UpdatePopupAfflictions() { - if (selectedCrewAfflictionList is { } afflictionList) - { - foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) - { - ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); - if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) - { - ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); - } - } + if (selectedCrewAfflictionList is not { } afflictionList) { return; } - afflictionList.TreatAllButton.Enabled = true; - if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) { - afflictionList.TreatAllButton.Enabled = false; + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); } } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } } private void UpdatePending() @@ -309,7 +307,7 @@ namespace Barotrauma } } - private void UpdateCrewPanel() + public void UpdateCrewPanel() { if (crewHealList is not { } healList) { return; } @@ -502,7 +500,7 @@ namespace Barotrauma return true; } }; - + crewHealList = new CrewHealList(crewList, parent, treatAllButton); void OnReceived(MedicalClinic.CallbackOnlyRequest obj) @@ -789,7 +787,7 @@ namespace Barotrauma GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; - PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, afflictionList, treatAllButton); selectedCrewElement = mainFrame; selectedCrewAfflictionList = popupAfflictionList; @@ -810,9 +808,9 @@ namespace Barotrauma List allComponents = new List(); foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) { - ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); - allComponents.AddRange(createdComponents); - popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents.AllCreatedElements); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, affliction)); } allComponents.Add(treatAllButton); @@ -832,9 +830,11 @@ namespace Barotrauma } } - private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + private readonly record struct CreatedPopupAfflictionElement(GUIComponent MainElement, ImmutableArray AllCreatedElements); + + private CreatedPopupAfflictionElement CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) { - if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + ToolBox.ThrowIfNull(affliction.Prefab); GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); @@ -846,9 +846,9 @@ namespace Barotrauma GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; - Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + Color iconColor = CharacterHealth.GetAfflictionIconColor(affliction.Prefab, affliction.Strength); - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true) { Color = iconColor, DisabledColor = iconColor * 0.5f @@ -856,7 +856,7 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), affliction.Prefab.Name, font: GUIStyle.SubHeadingFont); Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -878,7 +878,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(affliction.Prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -923,7 +923,7 @@ namespace Barotrauma return true; }; - return elementsToDisable; + return new CreatedPopupAfflictionElement(backgroundFrame, elementsToDisable); } private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) @@ -1033,11 +1033,53 @@ namespace Barotrauma } } + public void UpdateAfflictions(MedicalClinic.NetCrewMember crewMember) + { + if (selectedCrewAfflictionList is not { } afflictionList || !afflictionList.Target.CharacterEquals(crewMember)) { return; } + + List allComponents = new List(); + foreach (PopupAffliction existingAffliction in afflictionList.Afflictions.ToHashSet()) + { + if (crewMember.Afflictions.None(received => received.AfflictionEquals(existingAffliction.Target))) + { + // remove from UI + existingAffliction.TargetElement.RectTransform.Parent = null; + afflictionList.Afflictions.Remove(existingAffliction); + } + else + { + allComponents.AddRange(existingAffliction.ElementsToDisable); + } + } + + foreach (MedicalClinic.NetAffliction received in crewMember.Afflictions) + { + // we're not that concerned about updating the strength of the afflictions + if (afflictionList.Afflictions.Any(existing => existing.Target.AfflictionEquals(received))) { continue; } + + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.ListElement.Content, crewMember, received); + allComponents.AddRange(createdComponents.AllCreatedElements); + afflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, received)); + } + + allComponents.Add(afflictionList.TreatAllButton); + afflictionList.TreatAllButton.OnClicked = (_, _) => + { + var afflictions = crewMember.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + UpdatePopupAfflictions(); + } + public void ClosePopup() { if (selectedCrewElement is { } popup) { - popup.Parent?.RemoveChild(selectedCrewElement); + popup.RectTransform.Parent = null; } selectedCrewElement = null; @@ -1096,5 +1138,14 @@ namespace Barotrauma refreshTimer = 0; } } + + public void OnDeselected() + { + if (GameMain.NetworkMember is not null) + { + MedicalClinic.SendUnsubscribeRequest(); + } + ClosePopup(); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 206fcb171..1cb5e37ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -207,11 +207,12 @@ namespace Barotrauma cargoManager.OnItemsInSellFromSubCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingFromSubRefresh = true); } - public void SelectStore(Identifier identifier) + public void SelectStore(Character merchant) { + Identifier storeIdentifier = merchant?.MerchantIdentifier ?? Identifier.Empty; if (CurrentLocation?.Stores != null) { - if (!identifier.IsEmpty && CurrentLocation.GetStore(identifier) is { } store) + if (!storeIdentifier.IsEmpty && CurrentLocation.GetStore(storeIdentifier) is { } store) { ActiveStore = store; if (storeNameBlock != null) @@ -223,12 +224,13 @@ namespace Barotrauma } storeNameBlock.SetRichText(storeName); } + ActiveStore.SetMerchantFaction(merchant.Faction); } else { ActiveStore = null; string errorId, msg; - if (identifier.IsEmpty) + if (storeIdentifier.IsEmpty) { errorId = "Store.SelectStore:IdentifierEmpty"; msg = $"Error selecting store at {CurrentLocation}: identifier is empty."; @@ -236,7 +238,7 @@ namespace Barotrauma else { errorId = "Store.SelectStore:StoreDoesntExist"; - msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; } DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); @@ -249,17 +251,17 @@ namespace Barotrauma if (campaignUI.Campaign.Map == null) { errorId = "Store.SelectStore:MapNull"; - msg = $"Error selecting store with identifier \"{identifier}\": Map is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": Map is null."; } else if (CurrentLocation == null) { errorId = "Store.SelectStore:CurrentLocationNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation is null."; } else if (CurrentLocation.Stores == null) { errorId = "Store.SelectStore:StoresNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation.Stores is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation.Stores is null."; } if (!msg.IsNullOrEmpty()) { @@ -406,11 +408,11 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) + if (ActiveStore is not null) { Color textColor = GUIStyle.ColorReputationNeutral; string sign = ""; - int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + int reputationModifier = (int)MathF.Round((ActiveStore.GetReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); if (reputationModifier > 0) { textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; @@ -1903,7 +1905,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.GetTooltip(); + toolTip = purchasedItem.ItemPrefab.GetTooltip(Character.Controlled); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 326768d32..d1ea71205 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -15,8 +15,6 @@ namespace Barotrauma private int pageCount; private readonly bool transferService, purchaseService; private bool initialized; - private int deliveryFee; - private string deliveryLocationName; public GUIFrame GuiFrame; private GUIFrame pageIndicatorHolder; @@ -34,14 +32,13 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, selectedSubText, switchText, missingPreviewText, currencyName; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, selectedSubText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; private readonly LocalizedString[] messageBoxOptions; - public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; private static readonly Color indicatorColor = new Color(112, 149, 129); @@ -108,14 +105,9 @@ namespace Barotrauma { initialized = true; selectedSubText = TextManager.Get("selectedsub"); - deliveryText = TextManager.Get("requestdeliverybutton"); switchText = TextManager.Get("switchtosubmarinebutton"); purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); purchaseOnlyText = TextManager.Get("purchase"); - if (transferService) - { - deliveryFee = CalculateDeliveryFee(); - } currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); @@ -124,13 +116,6 @@ namespace Barotrauma CreateGUI(); } - private int CalculateDeliveryFee() - { - int distanceToOutpost = GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - deliveryLocationName = endLocation.Name; - return DeliveryFeePerDistanceTravelled * distanceToOutpost; - } - private void CreateGUI() { createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -194,7 +179,7 @@ namespace Barotrauma 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"); + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : 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) @@ -413,15 +398,7 @@ namespace Barotrauma { if (subToDisplay.Name != CurrentOrPendingSubmarine().Name) { - if (deliveryFee > 0) - { - LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); - submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("deliveryfee", "[amount]", amountString); - } - else - { - submarineDisplays[i].submarineFee.Text = string.Empty; - } + submarineDisplays[i].submarineFee.Text = string.Empty; } else { @@ -581,7 +558,7 @@ namespace Barotrauma if (owned) { - confirmButton.Text = deliveryFee > 0 ? deliveryText : switchText; + confirmButton.Text = switchText; confirmButton.OnClicked = (button, userData) => { ShowTransferPrompt(); @@ -615,7 +592,7 @@ namespace Barotrauma listBackground.SetCrop(true); GUIFont font = GUIStyle.Font; - info.CreateSpecsWindow(specsFrame, font); + info.CreateSpecsWindow(specsFrame, font, includeCrushDepth: true); descriptionTextBlock.Text = info.Description; descriptionTextBlock.CalculateHeightFromText(); } @@ -702,37 +679,12 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (!GameMain.GameSession.Campaign.CanAfford(deliveryFee) && deliveryFee > 0) - { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyName), - ("[submarinename]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); - return; - } + var text = TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - GUIMessageBox msgBox; - - if (deliveryFee > 0) - { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", - ("[submarinename1]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyName)), messageBoxOptions); - msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; - } - else - { - var text = TextManager.GetWithVariables("switchsubmarinetext", - ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)); - text += GetItemTransferText(); - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -777,7 +729,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -856,7 +808,7 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 7abd6a444..6b8ccfbb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -836,7 +836,7 @@ namespace Barotrauma Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}"); campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { - if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } + if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; } SetWalletText(walletBlock, e.Wallet, icon, largeIcon); }); registeredEvents.Add(eventIdentifier); @@ -1786,7 +1786,10 @@ namespace Barotrauma { CurrentSelectMode = GUIListBox.SelectMode.None }; - sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); + sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, + includeTitle: false, + includeClass: false, + includeDescription: true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 94055e5b7..3bdf2d65d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -278,7 +278,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); - GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, GUI.IsFourByThree() ? 0.98f : 0.95f, parent, Anchor.Center), style: null); + GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, 0.95f, parent, Anchor.Center), style: null); mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f }; topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true); @@ -300,8 +300,8 @@ namespace Barotrauma new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; - GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; - GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; + GUIButton repairButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; /* RIGHT HEADER LAYOUT * |---------------------------------------------------------------------------------------------------| @@ -352,12 +352,15 @@ namespace Barotrauma SelectTab(UpgradeTab.Upgrade); - var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.27f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(GUI.IsFourByThree() ? 0.5f : 0.47f, 0.0f) }, DrawItemSwapPreview) + var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.25f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.52f * GUI.AspectRatioAdjustment, 0.0f) }, DrawItemSwapPreview) { IgnoreLayoutGroups = true, CanBeFocused = true }; + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, repairButton.TextBlock); + #if DEBUG // creates a button that re-creates the UI CreateRefreshButton(); @@ -730,7 +733,7 @@ namespace Barotrauma if (storeLayout == null || mainStoreLayout == null) { return; } currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); - selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; + selectedUpgradeCategoryLayout = new GUIFrame(rectT(0.3f * GUI.AspectRatioAdjustment, 1, mainStoreLayout), style: null) { CanBeFocused = false }; RefreshUpgradeList(); @@ -961,7 +964,7 @@ namespace Barotrauma bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); @@ -1001,7 +1004,7 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, price, replacement, addBuyButton: true, addProgressBar: false, @@ -1134,7 +1137,8 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.35f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + //name.RectTransform.MinSize = new Point(0, (int)name.TextSize.Y); GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; @@ -1176,7 +1180,7 @@ namespace Barotrauma materialCostList.Visible = false; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; - var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice) + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.CenterRight) { UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 4db7544b1..3a217bf1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -189,24 +189,10 @@ namespace Barotrauma ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - if (deliveryFee > 0) - { - tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString), - ("[locationname]", endLocation.Name), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", TextManager.Get("credit").ToLower())); - } - else - { - tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString)); - } + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); break; } votingOnText = RichString.Rich(text); @@ -241,25 +227,10 @@ namespace Barotrauma ("[novotecount]", noVoteCount.ToString())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - - if (deliveryFee > 0) - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", - ("[submarinename]", info.DisplayName), - ("[locationname]", endLocation.Name), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", deliveryFee)), - ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } - else - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", - ("[submarinename]", info.DisplayName), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); break; default: break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index c15b9bc19..4ec0e6505 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -17,14 +17,19 @@ using System.Linq; using System.Reflection; using System.Threading; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { class GameMain : Game { - public static bool ShowFPS = false; - public static bool ShowPerf = false; + public static bool ShowFPS; + public static bool ShowPerf; public static bool DebugDraw; + /// + /// Doesn't automatically enable los or bot AI or do anything like that. Probably not fully implemented. + /// + public static bool DevMode; public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; @@ -227,9 +232,8 @@ namespace Barotrauma } GameSettings.Init(); + CreatureMetrics.Init(); - Md5Hash.Cache.Load(); - ConsoleArguments = args; try @@ -397,7 +401,7 @@ namespace Barotrauma TextureLoader.Init(GraphicsDevice); //do this here because we need it for the loading screen - WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); + WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice); Quad.Init(GraphicsDevice); @@ -475,6 +479,19 @@ namespace Barotrauma yield return CoroutineStatus.Running; } + var corePackage = ContentPackageManager.EnabledPackages.Core; + if (corePackage.EnableError.TryUnwrap(out var error)) + { + if (error.ErrorsOrException.TryGet(out ImmutableArray errorMessages)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {errorMessages.First()}"); + } + else if (error.ErrorsOrException.TryGet(out Exception exception)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {exception.Message}", exception); + } + } + TextManager.VerifyLanguageAvailable(); DebugConsole.Init(); @@ -498,10 +515,10 @@ namespace Barotrauma TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; - GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice, Content); + GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice); ParticleManager = new ParticleManager(GameScreen.Cam); - LightManager = new Lights.LightManager(base.GraphicsDevice, Content); + LightManager = new Lights.LightManager(base.GraphicsDevice); TitleScreen.LoadState = 80.0f; yield return CoroutineStatus.Running; @@ -729,7 +746,7 @@ namespace Barotrauma } else if (HasLoaded) { - if (ConnectCommand is Some { Value: var connectCommand }) + if (ConnectCommand.TryUnwrap(out var connectCommand)) { if (Client != null) { @@ -1051,6 +1068,7 @@ namespace Barotrauma public static void QuitToMainMenu(bool save) { + CreatureMetrics.Save(); if (save) { GUI.SetSavingIndicatorState(true); @@ -1156,6 +1174,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { exiting = true; + CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); SteamManager.ShutDown(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 0969b5574..377bba240 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -193,7 +193,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -787,7 +787,6 @@ namespace Barotrauma { return; } - if (ws != null) { hull = Hull.FindHull(ws.WorldPosition); @@ -801,7 +800,6 @@ namespace Barotrauma hull = Hull.FindHull(se.WorldPosition); } } - if (IsSinglePlayer) { order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); @@ -816,13 +814,13 @@ namespace Barotrauma { //can't issue an order if no characters are available if (character == null) { return; } - var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, isNewOrder, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); - orderGiver?.Speak(message); + bool isGivingOrderToSelf = orderGiver == character; + character.SetOrder(order, isNewOrder, speak: !isGivingOrderToSelf); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, isGivingOrderToSelf, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); + orderGiver?.Speak(message); } else if (orderGiver != null) { @@ -1404,8 +1402,7 @@ namespace Barotrauma bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && - (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton == MouseButton.PrimaryMouse; bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -2797,8 +2794,8 @@ namespace Barotrauma var orderName = GetOrderNameBasedOnContextuality(order); var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, tooltip: !showAssignmentTooltip ? orderName : orderName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); if (disableNode) { @@ -3000,8 +2997,8 @@ namespace Barotrauma var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); } if (!CanCharacterBeHeard()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 4a91c9026..d004cd9bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { - if (Owner is Some { Value: var character }) + if (Owner.TryUnwrap(out var character)) { if (!character.IsPlayer) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 500250be8..61feab4eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -33,7 +33,7 @@ namespace Barotrauma protected set; } - public static CancellationTokenSource StartRoundCancellationToken { get; private set; } + private CancellationTokenSource startRoundCancellationToken; public bool ForceMapUI { @@ -62,10 +62,19 @@ namespace Barotrauma { chatBox.ToggleOpen = wasChatBoxOpen; } - if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) + if (!value) { - SubmarinePreview.Close(); + switch (CampaignUI?.SelectedTab) + { + case InteractionType.PurchaseSub: + SubmarinePreview.Close(); + break; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic?.OnDeselected(); + break; + } } + showCampaignUI = value; } } @@ -110,12 +119,7 @@ namespace Barotrauma public static bool AllowedToManageWallets() { - if (GameMain.Client == null) { return true; } - - return - GameMain.Client.HasPermission(ClientPermissions.ManageMoney) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.IsServerOwner; + return AllowedToManageCampaign(ClientPermissions.ManageMoney); } public override void Draw(SpriteBatch spriteBatch) @@ -247,11 +251,11 @@ namespace Barotrauma GUI.ClearCursorWait(); - StartRoundCancellationToken = new CancellationTokenSource(); + startRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); - Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; + Rand.ThreadId = Environment.CurrentManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); @@ -261,7 +265,8 @@ namespace Barotrauma roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; - }, StartRoundCancellationToken.Token); + startRoundCancellationToken = null; + }, startRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; @@ -271,6 +276,21 @@ namespace Barotrauma return loadTask; } + public void CancelStartRound() + { + startRoundCancellationToken?.Cancel(); + } + + public void ThrowIfStartRoundCancellationRequested() + { + if (startRoundCancellationToken != null && + startRoundCancellationToken.Token.IsCancellationRequested) + { + startRoundCancellationToken.Token.ThrowIfCancellationRequested(); + startRoundCancellationToken = null; + } + } + protected SubmarineInfo GetPredefinedStartOutpost() { if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) @@ -304,7 +324,7 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); + CampaignUI.SelectTab(npc.CampaignInteractionType, npc); CampaignUI.UpgradeStore?.RequestRefresh(); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 959287931..53cf2e86b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -962,25 +962,20 @@ namespace Barotrauma foreach (NetWalletTransaction transaction in update.Transactions) { WalletInfo info = transaction.Info; - switch (transaction.CharacterID) + if (transaction.CharacterID.TryUnwrap(out var charID)) { - case Some { Value: var charID }: - { - Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); - if (targetCharacter is null) { break; } - Wallet wallet = targetCharacter.Wallet; + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; - wallet.Balance = info.Balance; - wallet.RewardDistribution = info.RewardDistribution; - TryInvokeEvent(wallet, transaction.ChangedData, info); - break; - } - case None _: - { - Bank.Balance = info.Balance; - TryInvokeEvent(Bank, transaction.ChangedData, info); - break; - } + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + } + else + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); } } @@ -995,7 +990,7 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { - if (!AllowedToManageCampaign(ClientPermissions.ManageCampaign)) + if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 6173482f8..102c9d921 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -421,6 +421,7 @@ namespace Barotrauma TotalPassedLevels++; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; case TransitionType.End: @@ -437,9 +438,9 @@ namespace Barotrauma if (transitionType != TransitionType.End) { var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, - transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, - fadeOut: false, - panDuration: EndTransitionDuration); + transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, + fadeOut: false, + panDuration: EndTransitionDuration); Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index fcb5438dc..49f2a6fcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -585,7 +585,7 @@ namespace Barotrauma if (!gap.IsRoomToRoom) { if (!IsWearingDivingSuit()) { continue; } - if (Character.Controlled.IsProtectedFromPressure()) { continue; } + if (Character.Controlled.IsProtectedFromPressure) { continue; } if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 232e84838..3b4d31cf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -11,6 +11,8 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic; + public enum RequestResult { Undecided, @@ -303,6 +305,12 @@ namespace Barotrauma } } + private void AfflictionUpdateReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + ui?.UpdateAfflictions(crewMember); + } + private void PendingRequestReceived(IReadMessage inc) { var pendingCrew = INetSerializableStruct.Read>(inc); @@ -312,6 +320,10 @@ namespace Barotrauma } } + public static void SendUnsubscribeRequest() => ClientSend(null, + header: NetworkHeader.UNSUBSCRIBE_ME, + deliveryMethod: DeliveryMethod.Reliable); + private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); @@ -337,6 +349,9 @@ namespace Barotrauma case NetworkHeader.REQUEST_AFFLICTIONS: AfflictionRequestReceived(inc); break; + case NetworkHeader.AFFLICTION_UPDATE: + AfflictionUpdateReceived(inc); + break; case NetworkHeader.REQUEST_PENDING: PendingRequestReceived(inc); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 9da10c685..cabf5bcbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -40,7 +40,7 @@ namespace Barotrauma private void CreateMessageBox(string author) { - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); + Vector2 relativeSize = new Vector2(0.3f * GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 949500e0a..1f55a4ff8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -311,7 +311,6 @@ namespace Barotrauma } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); - int reward = displayedMission.GetReward(Submarine.MainSub); if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) { RichString reputationText = displayedMission.GetReputationRewardText(); @@ -320,12 +319,13 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText); } - if (reward > 0) + int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + if (totalReward > 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); @@ -419,7 +419,7 @@ namespace Barotrauma var factionFrame = CreateReputationElement( reputationList.Content, faction.Prefab.Name, - faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Reputation, initialReputation, faction.Prefab.ShortDescription, faction.Prefab.Description, faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); CreatePathUnlockElement(factionFrame, faction, null); @@ -685,7 +685,7 @@ namespace Barotrauma } private GUIFrame CreateReputationElement(GUIComponent parent, - LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -703,21 +703,22 @@ namespace Barotrauma }; } - var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) + var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterRight, isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; + var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) + { + Color = iconColor + }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; - var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) - { - Color = iconColor - }; + factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), @@ -738,24 +739,30 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + + var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + SetReputationText(reputationText); + reputation?.OnReputationValueChanged.RegisterOverwriteExisting("RefreshRoundSummary".ToIdentifier(), _ => + { + SetReputationText(reputationText); + }); - LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); - int reputationChange = (int)Math.Round(reputation - initialReputation); - if (Math.Abs(reputationChange) > 0) + void SetReputationText(GUITextBlock textBlock) { - string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); - var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - richText, - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - RichString.Rich(reputationText), - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); + int reputationChange = (int)Math.Round(reputation.Value - initialReputation); + if (Math.Abs(reputationChange) > 0) + { + string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + textBlock.Text = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); + } + else + { + textBlock.Text = RichString.Rich(reputationText); + } } //spacing diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 11530061e..e388ac515 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -63,7 +63,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; - public static int Spacing; private Layout layout; public Layout CurrentLayout @@ -103,7 +102,7 @@ namespace Barotrauma { visualSlots ??= new VisualSlot[capacity]; - float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; + float multiplier = UIScale * GUI.AspectRatioAdjustment; for (int i = 0; i < capacity; i++) { @@ -219,18 +218,11 @@ namespace Barotrauma private void SetSlotPositions(Layout layout) { - bool isFourByThree = GUI.IsFourByThree(); - if (isFourByThree) - { - Spacing = (int)(5 * UIScale); - } - else - { - Spacing = (int)(8 * UIScale); - } + int spacing = GUI.IntScale(5); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); - int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; + SlotSize = (SlotSpriteSmall.size * UIScale * GUI.AspectRatioAdjustment).ToPoint(); + int bottomOffset = SlotSize.Y + spacing * 2 + ContainedIndicatorHeight; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - spacing * 2 - (int)(UnequippedIndicator.size.Y * UIScale); if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -242,11 +234,11 @@ namespace Barotrauma int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); 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; + int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + spacing) / 2; + 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); + x -= Math.Max((x + normalSlotCount * (SlotSize.X + spacing)) - (upperX - personalSlotCount * (SlotSize.X + spacing)), 0); int hideButtonSlotIndex = -1; for (int i = 0; i < SlotPositions.Length; i++) @@ -254,7 +246,7 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(upperX, GameMain.GraphicsHeight - bottomOffset); - upperX -= SlotSize.X + Spacing; + upperX -= SlotSize.X + spacing; personalSlotArea = (hideButtonSlotIndex == -1) ? new Rectangle(SlotPositions[i].ToPoint(), SlotSize) : Rectangle.Union(personalSlotArea, new Rectangle(SlotPositions[i].ToPoint(), SlotSize)); @@ -263,7 +255,7 @@ namespace Barotrauma else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += SlotSize.X + Spacing; + x += SlotSize.X + spacing; } } } @@ -271,7 +263,7 @@ namespace Barotrauma case Layout.Right: { int x = HUDLayoutSettings.InventoryAreaLower.Right; - int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; + int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - spacing; for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } @@ -282,19 +274,18 @@ namespace Barotrauma } else { - x -= SlotSize.X + Spacing; + x -= SlotSize.X + spacing; } } int lowerX = x; int handSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { SlotPositions[i] = new Vector2(handSlotX, personalSlotY); - handSlotX += visualSlots[i].Rect.Width + Spacing; + handSlotX += visualSlots[i].Rect.Width + spacing; continue; } @@ -302,12 +293,12 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX -= visualSlots[i].Rect.Width + Spacing; + personalSlotX -= visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } @@ -316,7 +307,7 @@ namespace Barotrauma { if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } - x -= visualSlots[i].Rect.Width + Spacing; + x -= visualSlots[i].Rect.Width + spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } @@ -325,7 +316,6 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { @@ -334,33 +324,33 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX += visualSlots[i].Rect.Width + Spacing; + personalSlotX += visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - int handSlotX = x - visualSlots[0].Rect.Width - Spacing; + int handSlotX = x - visualSlots[0].Rect.Width - spacing; for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; - SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - spacing, personalSlotY); continue; } if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } break; case Layout.Center: { int columns = 5; - int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + Spacing * (columns - 1)) / 2; + int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + spacing * (columns - 1)) / 2; int startY = GameMain.GraphicsHeight / 2 - (SlotSize.Y * 2); int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) @@ -369,10 +359,10 @@ namespace Barotrauma if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - y += visualSlots[0].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; + y += visualSlots[0].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; x = startX; int n = 0; for (int i = 0; i < SlotPositions.Length; i++) @@ -381,12 +371,12 @@ namespace Barotrauma if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; n++; if (n >= columns) { x = startX; - y += visualSlots[i].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; + y += visualSlots[i].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; n = 0; } } @@ -402,7 +392,7 @@ namespace Barotrauma { if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } SlotPositions[i] = pos; - pos.Y += visualSlots[i].Rect.Height + Spacing; + pos.Y += visualSlots[i].Rect.Height + spacing; } } @@ -641,7 +631,7 @@ namespace Barotrauma { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { slot.EquipButtonState = GUIComponent.ComponentState.None; } @@ -965,7 +955,7 @@ namespace Barotrauma break; case QuickUseAction.PutToEquippedItem: //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank - foreach (Item heldItem in character.HeldItems.OrderBy(it => it.GetComponent()?.GetContainedIndicatorState() ?? 0.0f)) + foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem))) { 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 @@ -986,6 +976,22 @@ namespace Barotrauma } } break; + + static float GetContainPriority(Item item, Item containerItem) + { + var container = containerItem.GetComponent(); + if (container == null) { return 0.0f; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + var containedItems = container.Inventory.GetItemsAt(i); + if (containedItems.Any() && container.Inventory.CanBePutInSlot(item, i)) + { + //if there's a stack in the contained item that we can add the item to, prefer that + return 10.0f; + } + } + return -container.GetContainedIndicatorState(); + } } if (success) @@ -1002,7 +1008,47 @@ namespace Barotrauma SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - + + public bool CanBeAutoMovedToCorrectSlots(Item item) + { + if (item == null) { return false; } + foreach (var allowedSlot in item.AllowedSlots) + { + InvSlotType slotsFree = InvSlotType.None; + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } + } + if (allowedSlot == slotsFree) { return true; } + } + return false; + } + + /// + /// Flash the slots the item is allowed to go in (not taking into account whether there's already something in those slots) + /// + public void FlashAllowedSlots(Item item, Color color) + { + if (item == null || visualSlots == null) { return; } + bool flashed = false; + foreach (var allowedSlot in item.AllowedSlots) + { + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i])) + { + visualSlots[i].ShowBorderHighlight(color, 0.1f, 0.9f); + flashed = true; + } + } + } + if (flashed) + { + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } + } + + public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } @@ -1090,40 +1136,24 @@ namespace Barotrauma color *= 0.5f; } - if (character.HasEquippedItem(slots[i].First())) + Vector2 indicatorScale = new Vector2( + visualSlots[i].EquipButtonRect.Size.X / EquippedIndicator.size.X, + visualSlots[i].EquipButtonRect.Size.Y / EquippedIndicator.size.Y); + + bool isEquipped = character.HasEquippedItem(slots[i].First()); + var sprite = state switch { - switch (state) - { - case GUIComponent.ComponentState.None: - EquippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - EquippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - EquippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } - else - { - switch (state) - { - case GUIComponent.ComponentState.None: - UnequippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - UnequippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - UnequippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } + GUIComponent.ComponentState.None + => isEquipped ? EquippedIndicator : UnequippedIndicator, + GUIComponent.ComponentState.Hover + => isEquipped ? EquippedHoverIndicator : UnequippedHoverIndicator, + GUIComponent.ComponentState.Pressed + or GUIComponent.ComponentState.Selected + or GUIComponent.ComponentState.HoverSelected + => isEquipped ? EquippedClickedIndicator : UnequippedClickedIndicator, + _ => throw new NotImplementedException() + }; + sprite.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, indicatorScale); } if (Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index aa7baf8be..e5b132f89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -182,7 +182,7 @@ namespace Barotrauma.Items.Components if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured - color *= (item.Condition / item.MaxCondition); + color = color.Multiply(item.Condition / item.MaxCondition); color.A = 255; } @@ -216,16 +216,19 @@ namespace Barotrauma.Items.Components 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 (doorSprite?.Texture != null) + { + spriteBatch.Draw(doorSprite.Texture, pos, + getSourceRect(doorSprite, openState, IsHorizontal), + color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); + } } float maxCondition = item.Repairables.Any() ? item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition : item.MaxCondition; float healthRatio = item.Health / maxCondition; - if (brokenSprite != null && healthRatio < 1.0f) + if (brokenSprite?.Texture != null && healthRatio < 1.0f) { Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One; if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } @@ -285,34 +288,45 @@ namespace Barotrauma.Items.Components //sent by the server, or reverting it back to its old state if no msg from server was received PredictedState = open; resetPredictionTimer = CorrectionDelay; - if (stateChanged) PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); + if (stateChanged && !IsBroken) + { + PlayInteractionSound(); + } } else { isOpen = open; if (!isNetworkMessage || open != PredictedState) { - StopPicking(null); - ActionType actionType = ActionType.OnUse; - if (forcedOpen) + StopPicking(null); + if (!IsBroken) { - actionType = ActionType.OnPicked; + PlayInteractionSound(); } - else - { - if (open && HasSoundsOfType[(int)ActionType.OnOpen]) - { - actionType = ActionType.OnOpen; - } - else if (!open && HasSoundsOfType[(int)ActionType.OnClose]) - { - actionType = ActionType.OnClose; - } - } - PlaySound(actionType); if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } - } + } + + void PlayInteractionSound() + { + ActionType actionType = ActionType.OnUse; + if (forcedOpen) + { + actionType = ActionType.OnPicked; + } + else + { + if (open && HasSoundsOfType[(int)ActionType.OnOpen]) + { + actionType = ActionType.OnOpen; + } + else if (!open && HasSoundsOfType[(int)ActionType.OnClose]) + { + actionType = ActionType.OnClose; + } + } + PlaySound(actionType); + } } public override void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index fd8f360eb..2f23d59d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,13 +1,10 @@ using Barotrauma.Particles; +using Barotrauma.Sounds; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Text; -using System.Xml.Linq; -using Barotrauma.Sounds; using System.Linq; namespace Barotrauma.Items.Components @@ -169,11 +166,11 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - float rotation = -item.body.Rotation; + float rotation = item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: -rotation); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 5f1a13b2f..ebd078a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -505,13 +505,14 @@ namespace Barotrauma.Items.Components } ActionType type; + string typeStr = subElement.GetAttributeString("type", ""); try { - type = (ActionType)Enum.Parse(typeof(ActionType), subElement.GetAttributeString("type", ""), true); + type = (ActionType)Enum.Parse(typeof(ActionType), typeStr, true); } catch (Exception e) { - DebugConsole.ThrowError("Invalid sound type in " + subElement + "!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5b16c7d9c..17293bf7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -277,7 +277,8 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float capacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = GetMaxStackSize(targetSlot); + float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { bool useMainContainerCapacity = true; @@ -299,15 +300,11 @@ namespace Barotrauma.Items.Components } if (!useMainContainerCapacity) { break; } } - if (useMainContainerCapacity) - { - capacity *= MainContainerCapacity; - } - else + if (!useMainContainerCapacity) { // Ignore all items in the main container. ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= Capacity - MainContainerCapacity; + capacity = targetSlotCapacity * (Capacity - MainContainerCapacity); } } int itemCount = Inventory.AllItems.Count() - ignoredItemCount; @@ -391,63 +388,60 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (Item containedItem in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(containedItem); - if (relatedItem != null) + + if (contained.Item?.Sprite == null) { continue; } + + if (contained.Hide) { continue; } + if (contained.ItemPos.HasValue) { - if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (item.Submarine != null) - { - itemPos += item.Submarine.DrawPosition; - } - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; } } } - - if (containedItem?.Sprite == null) { continue; } - + if (AutoInteractWithContained) { - containedItem.IsHighlighted = item.IsHighlighted; + contained.Item.IsHighlighted = item.IsHighlighted; item.IsHighlighted = false; } - Vector2 origin = containedItem.Sprite.Origin; - if (item.FlippedX) { origin.X = containedItem.Sprite.SourceRect.Width - origin.X; } - if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } + Vector2 origin = contained.Item.Sprite.Origin; + if (item.FlippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; } + if (item.FlippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } - float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; if (i < containedSpriteDepths.Length) { containedSpriteDepth = containedSpriteDepths[i]; @@ -456,9 +450,9 @@ namespace Barotrauma.Items.Components SpriteEffects spriteEffects = SpriteEffects.None; float spriteRotation = ItemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - spriteRotation = relatedItem.Rotation; + spriteRotation = contained.Rotation; } if ((item.body != null && item.body.Dir == -1) || item.FlippedX) { @@ -469,17 +463,17 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } - containedItem.Sprite.Draw( + contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), + isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), - containedItem.Scale, + -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - foreach (ItemContainer ic in containedItem.GetComponents()) + foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index b8d40669d..2c764937c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -78,14 +78,21 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) + if (Light?.LightSprite == null) { return; } + if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) { Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition; - Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + + Color color = lightColor; + if (Light.OverrideLightSpriteAlpha.HasValue) + { + color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); + } + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3debe66f7..8e30123bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -185,6 +185,7 @@ namespace Barotrauma.Items.Components RefreshActivateButtonText(); if (GameMain.Client != null) { + pendingFabricatedItem = null; item.CreateClientEvent(this); } return true; @@ -336,8 +337,11 @@ namespace Barotrauma.Items.Components int calculatePlacement(FabricationRecipe recipe) { + if (recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem)) + { + return -2; + } int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; - placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; return placement; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 85ac7fd01..abe3fd223 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -413,7 +413,7 @@ namespace Barotrauma.Items.Components var wire = it.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } - if (it.Container?.GetComponent() is { DrawInventory: false }) { return false; } + if (it.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } if (it.HasTag("traitormissionitem")) { return false; } @@ -452,7 +452,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, component) in electricalMapComponents) { GUIComponent parent = component.RectComponent; - if (!(entity is Item it )) { continue; } + if (entity is not Item it ) { continue; } Sprite? sprite = it.Prefab.UpgradePreviewSprite; if (sprite is null) { continue; } @@ -476,7 +476,7 @@ namespace Barotrauma.Items.Components { if (!hullPointsOfInterest.Contains(entity)) { continue; } - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } const int borderMaxSize = 2; if (it.GetComponent() is { }) @@ -643,7 +643,7 @@ namespace Barotrauma.Items.Components elementSize = GuiFrame.Rect.Size; } - float distort = 1.0f - item.Condition / item.MaxCondition; + float distort = item.Repairables.Any(r => r.IsBelowRepairThreshold) ? 1.0f - item.Condition / item.MaxCondition : 0.0f; foreach (HullData hullData in hullDatas.Values) { hullData.DistortionTimer -= deltaTime; @@ -1130,7 +1130,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) @@ -1220,7 +1220,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, component) in hullStatusComponents) { - if (!(entity is Hull hull)) { continue; } + if (entity is not Hull hull) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } if (hullData.Distort) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 9705b407b..56194db32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -72,7 +72,9 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; public bool TriggerInfographic { get; set; } - + + public bool IsInfographicVisible => infographic != null && infographic.Visible; + partial void InitProjSpecific(ContentXElement element) { CreateGUI(); @@ -108,6 +110,9 @@ namespace Barotrauma.Items.Components { AbsoluteOffset = GUIStyle.ItemFrameOffset }, isHorizontal: true) { + CanBeFocused = true, + HoverCursor = CursorState.Default, + AlwaysOverrideCursor = true, RelativeSpacing = 0.012f, Stretch = true }; @@ -675,7 +680,7 @@ namespace Barotrauma.Items.Components } } - if (TriggerInfographic) + if (GuiFrame is not null && GuiFrame.Visible && TriggerInfographic) { CreateInfrographic(); TriggerInfographic = false; @@ -851,8 +856,9 @@ namespace Barotrauma.Items.Components { AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale) }; - new GUIButton(closeButtonRt, TextManager.Get("close")) + new GUIButton(closeButtonRt, TextManager.Get("closeinfographic")) { + UserData = UIHighlightAction.ElementId.CloseButton, OnClicked = (_, _) => { CloseInfographic(Character.Controlled); @@ -871,6 +877,7 @@ namespace Barotrauma.Items.Components string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved"; return new GUIImage(rt, style) { + CanBeFocused = false, Rotation = MathHelper.ToRadians(rotationDegrees), SpriteEffects = spriteEffects }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 28a25c88e..6a2b6571b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,6 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); + SpreadCounter = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index afaeb2dca..c25a7c6cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -262,9 +262,11 @@ namespace Barotrauma.Items.Components } } + float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + for (int i = 0; i < particleEmitters.Count; i++) { - if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) + if ((conditionPercentage >= particleEmitterConditionRanges[i].X && conditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } @@ -436,12 +438,16 @@ namespace Barotrauma.Items.Components ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; - item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); - if (CurrentFixer == null) + + if (CurrentFixer is null) { qteTimer = QteDuration; qteCooldown = 0.0f; } + else + { + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 9526f7f63..5bee8bba0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components { startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } + startPos -= turret.GetRecoilOffset(); } else if (weapon != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 915e8f695..2525f4ffb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -110,8 +110,8 @@ namespace Barotrauma.Items.Components if (HighlightedWire != null) { HighlightedWire.Item.IsHighlighted = true; - if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) HighlightedWire.Connections[0].Item.IsHighlighted = true; - if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) HighlightedWire.Connections[1].Item.IsHighlighted = true; + if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) { HighlightedWire.Connections[0].Item.IsHighlighted = true; } + if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) { HighlightedWire.Connections[1].Item.IsHighlighted = true; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 17b151087..29b3adae2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -329,6 +329,7 @@ namespace Barotrauma.Items.Components partial void UpdateSignalsProjSpecific() { + if (signals == null) { return; } for (int i = 0; i < signals.Length && i < uiElements.Count; i++) { if (uiElements[i] is GUITextBox tb) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index c97a2945e..6ec91dabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -25,14 +25,14 @@ namespace Barotrauma.Items.Components public static Color editorHighlightColor = Color.Yellow; public static Color editorSelectedColor = Color.Red; - partial class WireSection + public partial class WireSection { public VertexPositionColorTexture[] vertices; public VertexPositionColorTexture[] shiftedVertices; private float cachedWidth = 0f; - private void RecalculateVertices(Wire wire, float width) + private void RecalculateVertices(Sprite wireSprite, float width) { if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } cachedWidth = width; @@ -45,13 +45,13 @@ namespace Barotrauma.Items.Components expandDir.X = -expandDir.Y; expandDir.Y = -temp; - Rectangle srcRect = wire.wireSprite.SourceRect; + Rectangle srcRect = wireSprite.SourceRect; expandDir *= width * srcRect.Height * 0.5f; Vector2 rectLocation = srcRect.Location.ToVector2(); Vector2 rectSize = srcRect.Size.ToVector2(); - Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); + Vector2 textureSize = new Vector2(wireSprite.Texture.Width, wireSprite.Texture.Height); Vector2 topLeftUv = rectLocation / textureSize; Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; @@ -67,10 +67,10 @@ namespace Barotrauma.Items.Components shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); } - public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) + public void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Color color, Vector2 offset, float depth, float width = 0.3f) { if (width <= 0f) { return; } - RecalculateVertices(wire, width); + RecalculateVertices(wireSprite, width); for (int i = 0; i < vertices.Length; i++) { @@ -79,21 +79,22 @@ namespace Barotrauma.Items.Components shiftedVertices[i].Position.X += offset.X; shiftedVertices[i].Position.Y -= offset.Y; } - spriteBatch.Draw(wire.wireSprite.Texture, + spriteBatch.Draw( + wireSprite.Texture, shiftedVertices, depth); } - public static void Draw(SpriteBatch spriteBatch, Wire wire, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) + public static void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) { start.Y = -start.Y; end.Y = -end.Y; - spriteBatch.Draw(wire.wireSprite.Texture, - start, wire.wireSprite.SourceRect, color, + spriteBatch.Draw(wireSprite.Texture, + start, wireSprite.SourceRect, color, MathUtils.VectorToAngle(end - start), - new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), - new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.size.X, width), + new Vector2(0.0f, wireSprite.size.Y / 2.0f), + new Vector2((Vector2.Distance(start, end)) / wireSprite.size.X, width), SpriteEffects.None, depth); } @@ -123,7 +124,7 @@ namespace Barotrauma.Items.Components get => draggingWire; } - partial void InitProjSpecific(ContentXElement element) + public static Sprite ExtractWireSprite(ContentXElement element) { if (defaultWireSprite == null) { @@ -133,6 +134,7 @@ namespace Barotrauma.Items.Components }; } + Sprite overrideSprite = null; foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) @@ -142,9 +144,14 @@ namespace Barotrauma.Items.Components } } - wireSprite = overrideSprite ?? defaultWireSprite; + return overrideSprite ?? defaultWireSprite; + } + + partial void InitProjSpecific(ContentXElement element) + { + wireSprite = ExtractWireSprite(element); + if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { @@ -181,20 +188,20 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } else if (item.IsSelected) { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, item.Color, drawOffset, depth, Width); + section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -239,13 +246,13 @@ namespace Barotrauma.Items.Components } WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.Color, 0.0f, Width); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, item.Color, itemDepth, Width); @@ -255,8 +262,8 @@ namespace Barotrauma.Items.Components else { WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, item.DrawPosition, item.Color, 0.0f, Width); } @@ -294,12 +301,12 @@ namespace Barotrauma.Items.Components Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f; WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, endPos, GUIStyle.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, start + (endPos - start) * 0.7f, item.Color, depth, 0.3f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index afe938020..9924b8ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -302,7 +302,18 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } + if (affliction.Strength <= 0f) { continue; } + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold) + { + if (target.IsHuman || target.IsOnPlayerTeam || (affliction.Prefab.AfflictionType != AfflictionPrefab.PoisonType && affliction.Prefab.AfflictionType != AfflictionPrefab.ParalysisType)) + { + // Always show the poisons on monsters, because poisoning bigger monsters require multiple doses. + // The solution is hacky, but didn't want to introduce an extra property for this. + // We also want to have a relatively high thershold for showing the poisons on the scanner on humans, so that it's not instantly clear that a target is poisoned and especially not which poison was used. + // Paralysis is treated like a poison but isn't technically a poison, so that we can have multiple afflictions that still are treated the same. + continue; + } + } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index e66a15de4..402805f70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -333,15 +333,9 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } - - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) - { - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) - { - UpdateTransformedBarrelPos(); - } - Vector2 drawPos = GetDrawPos(); + public Vector2 GetRecoilOffset() + { float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { @@ -362,6 +356,17 @@ namespace Barotrauma.Items.Components recoilOffset = RecoilDistance; } } + return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; + } + + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + { + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) + { + UpdateTransformedBarrelPos(); + } + Vector2 drawPos = GetDrawPos(); + railSprite?.Draw(spriteBatch, drawPos, @@ -370,7 +375,7 @@ namespace Barotrauma.Items.Components SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, - drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, + drawPos - GetRecoilOffset() * item.Scale, item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 58093440a..6693d594b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { - int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); + int roundedValue = (int)Math.Round((1 - Math.Min(damageModifier.DamageMultiplier, damageModifier.ProbabilityMultiplier)) * 100); if (roundedValue == 0) { return; } string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value); if (!description.IsNullOrWhiteSpace()) { description += '\n'; } - description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; + description += $" ‖color:{colorStr}‖{roundedValue:-0;+#}%‖color:end‖ {afflictionName}"; } public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) @@ -36,7 +36,6 @@ namespace Barotrauma.Items.Components { continue; } - foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) { GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 8728ef139..96607abbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -53,11 +53,11 @@ namespace Barotrauma get { // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen - if (IsMoving) return offScreenRect; + if (IsMoving) { return offScreenRect; } int buttonDir = Math.Sign(SubInventoryDir); - float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; + float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale; Vector2 equipIndicatorPos = new Vector2(Rect.Left, Rect.Center.Y + (Rect.Height / 2 + 15 * Inventory.UIScale) * buttonDir - sizeY / 2f); equipIndicatorPos += DrawOffset; @@ -176,14 +176,6 @@ namespace Barotrauma public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; - public static float IndicatorScaleAdjustment - { - get - { - return !GUI.IsFourByThree() ? 0.8f : 0.7f; - } - } - public static Inventory DraggingInventory; public Inventory ReplacedBy; @@ -249,11 +241,11 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - Tooltip = GetTooltip(Item, itemsInSlot); + Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private RichString GetTooltip(Item item, IEnumerable itemsInSlot) + private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) { if (item == null) { return null; } @@ -348,10 +340,12 @@ namespace Barotrauma } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + } + if (item.Prefab.SkillRequirementHints != null && item.Prefab.SkillRequirementHints.Any()) + { + toolTip += item.Prefab.GetSkillRequirementHints(character); } return RichString.Rich(toolTip); } @@ -576,7 +570,7 @@ namespace Barotrauma } } - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { mouseOn = false; } @@ -727,14 +721,7 @@ namespace Barotrauma Rectangle subRect = slot.Rect; Vector2 spacing; - if (GUI.IsFourByThree()) - { - spacing = new Vector2(5 * UIScale, (5 + UnequippedIndicator.size.Y) * UIScale); - } - else - { - spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); - } + spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale * GUI.AspectRatioAdjustment); int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) @@ -1535,16 +1522,6 @@ namespace Barotrauma { Sprite slotSprite = slot.SlotSprite ?? SlotSpriteSmall; - /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null - && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) - { - slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; - } - else - { - slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; - }*/ - if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); @@ -1713,6 +1690,15 @@ namespace Barotrauma GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } + + if (HealingCooldown.IsOnCooldown && item.HasTag(HealingCooldown.MedicalItemTag)) + { + RectangleF cdRect = rect; + // shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown + cdRect.Height *= HealingCooldown.NormalizedCooldown; + cdRect.Y += rect.Height; + GUI.DrawFilledRectangle(spriteBatch, cdRect, Color.White * 0.5f); + } } if (inventory != null && @@ -1722,7 +1708,17 @@ namespace Barotrauma slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); + + GUIStyle.HotkeyFont.DrawString( + spriteBatch, + GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, + rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), + Color.Black, + rotation: 0.0f, + origin: Vector2.Zero, + scale: Vector2.One * GUI.AspectRatioAdjustment, + SpriteEffects.None, + layerDepth: 0.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index f6ebf3b10..2ffbf71ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -23,7 +23,21 @@ namespace Barotrauma private readonly List activeEditors = new List(); - public GUIComponentStyle IconStyle { get; private set; } = null; + + private GUIComponentStyle iconStyle; + public GUIComponentStyle IconStyle + { + get { return iconStyle; } + private set + { + if (IconStyle != value) + { + iconStyle = value; + CheckIsHighlighted(); + } + } + } + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { if (interactionType == CampaignMode.InteractionType.None) @@ -143,6 +157,18 @@ namespace Barotrauma return color; } + protected override void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight || IconStyle != null) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + public Color GetInventoryIconColor() { Color color = InventoryIconColor; @@ -203,7 +229,7 @@ namespace Barotrauma } } - partial void InitProjSpecific() + public void InitSpriteStates() { Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); @@ -211,7 +237,6 @@ namespace Barotrauma { brokenSprite.Sprite.EnsureLazyLoaded(); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); @@ -221,6 +246,11 @@ namespace Barotrauma UpdateSpriteStates(0.0f); } + partial void InitProjSpecific() + { + InitSpriteStates(); + } + private Rectangle? cachedVisibleExtents; public void ResetCachedVisibleSize() @@ -729,7 +759,7 @@ namespace Barotrauma if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - var otherEntity = mapEntityList.FirstOrDefault(e => e != this && e.IsHighlighted && e.IsMouseOn(position)); + var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position)); if (otherEntity != null) { if (linkedTo.Contains(otherEntity)) @@ -1414,7 +1444,7 @@ namespace Barotrauma if (targetComponent == null) { - ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, true, worldPosition: worldPosition); + ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, isNetworkEvent: true, worldPosition: worldPosition); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index ee6bae0c0..49c3eb0e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -236,6 +236,16 @@ namespace Barotrauma DecorativeSprites = decorativeSprites.ToImmutableArray(); ContainedSprites = containedSprites.ToImmutableArray(); DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + +#if CLIENT + foreach (Item item in Item.ItemList) + { + if (item.Prefab == this) + { + item.InitSpriteStates(); + } + } +#endif } public bool CanCharacterBuy() @@ -244,7 +254,7 @@ namespace Barotrauma if (!DefaultPrice.RequiresUnlock) { return true; } return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); } - public LocalizedString GetTooltip() + public LocalizedString GetTooltip(Character character) { LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; if (!Description.IsNullOrEmpty()) @@ -255,6 +265,10 @@ namespace Barotrauma { Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); } + if (SkillRequirementHints != null && SkillRequirementHints.Any()) + { + tooltip += GetSkillRequirementHints(character); + } return tooltip; } @@ -366,5 +380,31 @@ namespace Barotrauma Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } + + public LocalizedString GetSkillRequirementHints(Character character) + { + LocalizedString text = ""; + if (SkillRequirementHints != null && SkillRequirementHints.Any() && character != null) + { + Color orange = GUIStyle.Orange; + // Reuse an existing, localized, text because it's what we want here: "Required skills:" + text = "\n\n" + $"‖color:{orange.ToStringHex()}‖{TextManager.Get("requiredrepairskills")}‖color:end‖"; + foreach (var hint in SkillRequirementHints) + { + int skillLevel = (int)character.GetSkillLevel(hint.Skill); + Color levelColor = GUIStyle.Yellow; + if (skillLevel >= hint.Level) + { + levelColor = GUIStyle.Green; + } + else if (skillLevel < hint.Level / 2) + { + levelColor = GUIStyle.Red; + } + text += "\n" + hint.GetFormattedText(skillLevel, levelColor.ToStringHex()); + } + } + return text; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 095195834..eac1396ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -129,20 +129,15 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted) { continue; } + if (entity == this) { continue; } if (!entity.IsMouseOn(position)) { continue; } if (entity.linkedTo == null || !entity.Linkable) { continue; } if (entity.linkedTo.Contains(this) || linkedTo.Contains(entity) || rClick) { - if (entity == this || !entity.IsHighlighted) { continue; } - if (!entity.IsMouseOn(position)) { continue; } - if (entity.linkedTo.Contains(this)) - { - entity.linkedTo.Remove(this); - linkedTo.Remove(entity); - } + entity.linkedTo.Remove(this); + linkedTo.Remove(entity); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index a053bcc73..9ce97aa94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -65,15 +64,9 @@ namespace Barotrauma public Texture2D WaterTexture { get; } - public WaterRenderer(GraphicsDevice graphicsDevice, ContentManager content) + public WaterRenderer(GraphicsDevice graphicsDevice) { -#if WINDOWS - WaterEffect = content.Load("Effects/watershader"); -#endif -#if LINUX || OSX - - WaterEffect = content.Load("Effects/watershader_opengl"); -#endif + WaterEffect = EffectLoader.Load("Effects/watershader"); WaterTexture = TextureLoader.FromFile("Content/Effects/waterbump.png"); WaterEffect.Parameters["xWaterBumpMap"].SetValue(WaterTexture); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 27b1aaf28..b4cf7304f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -2,7 +2,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using SharpFont; using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,28 +11,14 @@ namespace Barotrauma.Lights { class ConvexHullList { - private List list; - public HashSet IsHidden; public readonly Submarine Submarine; - public List List - { - get { return list; } - set - { - Debug.Assert(value != null); - Debug.Assert(!list.Contains(null)); - list = value; - IsHidden.RemoveWhere(ch => !list.Contains(ch)); - } - } - + public HashSet IsHidden = new HashSet(); + public readonly List List = new List(); public ConvexHullList(Submarine submarine) { Submarine = submarine; - list = new List(); - IsHidden = new HashSet(); } } @@ -354,7 +339,7 @@ namespace Barotrauma.Lights } } - public bool IsSegmentAInB(Segment a, Segment b) + public static bool IsSegmentAInB(Segment a, Segment b) { if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) { @@ -362,15 +347,16 @@ namespace Barotrauma.Lights } Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); - Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); min.X -= 1.0f; min.Y -= 1.0f; - max.X += 1.0f; max.Y += 1.0f; if (a.Start.Pos.X < min.X) { return false; } if (a.Start.Pos.Y < min.Y) { return false; } if (a.End.Pos.X < min.X) { return false; } if (a.End.Pos.Y < min.Y) { return false; } + Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); + max.X += 1.0f; max.Y += 1.0f; + if (a.Start.Pos.X > max.X) { return false; } if (a.Start.Pos.Y > max.Y) { return false; } if (a.End.Pos.X > max.X) { return false; } @@ -628,7 +614,7 @@ namespace Barotrauma.Lights { for (int i = 0; i < 4; i++) { - if (ignoreEdge[i] && ignoreEdges) continue; + if (ignoreEdge[i] && ignoreEdges) { continue; } Vector2 pos1 = vertices[i].WorldPos; Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e3aec5d91..4d734fc23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Content; using System.Collections.Generic; using System.Linq; using System; @@ -73,12 +72,14 @@ namespace Barotrauma.Lights private int recalculationCount; + private float time; + public IEnumerable Lights { get { return lights; } } - public LightManager(GraphicsDevice graphics, ContentManager content) + public LightManager(GraphicsDevice graphics) { lights = new List(100); @@ -96,13 +97,8 @@ namespace Barotrauma.Lights { CreateRenderTargets(graphics); -#if WINDOWS - LosEffect = content.Load("Effects/losshader"); - SolidColorEffect = content.Load("Effects/solidcolor"); -#else - LosEffect = content.Load("Effects/losshader_opengl"); - SolidColorEffect = content.Load("Effects/solidcolor_opengl"); -#endif + LosEffect = EffectLoader.Load("Effects/losshader"); + SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); if (lightEffect == null) { @@ -171,10 +167,12 @@ namespace Barotrauma.Lights public void Update(float deltaTime) { + //wrap around if the timer gets very large, otherwise we'd start running into floating point accuracy issues + time = (time + deltaTime) % 100000.0f; foreach (LightSource light in activeLights) { if (!light.Enabled) { continue; } - light.Update(deltaTime); + light.Update(time); } } @@ -451,9 +449,9 @@ namespace Barotrauma.Lights { highlightedEntities.Add(Character.Controlled.FocusedCharacter); } - foreach (Item item in Item.ItemList) + foreach (MapEntity me in MapEntity.HighlightedEntities) { - if ((item.IsHighlighted || item.IconStyle != null) && !highlightedEntities.Contains(item)) + if (me is Item item && item != Character.Controlled.FocusedItem) { highlightedEntities.Add(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 6fe45ed52..eaca44df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -200,12 +200,10 @@ namespace Barotrauma.Lights private static Texture2D lightTexture; - private float blinkTimer, flickerState, pulseState; - private VertexPositionColorTexture[] vertices; private short[] indices; - private readonly List hullsInRange; + private readonly List convexHullsInRange; public Texture2D texture; @@ -236,7 +234,7 @@ namespace Barotrauma.Lights { if (!needsRecalculation && value) { - foreach (ConvexHullList chList in hullsInRange) + foreach (ConvexHullList chList in convexHullsInRange) { chList.IsHidden.Clear(); } @@ -476,7 +474,7 @@ namespace Barotrauma.Lights public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) { - hullsInRange = new List(); + convexHullsInRange = new List(); this.ParentSub = submarine; this.position = position; lightSourceParams = new LightSourceParams(range, color); @@ -486,12 +484,12 @@ namespace Barotrauma.Lights if (addLight) { GameMain.LightManager.AddLight(this); } } - public void Update(float deltaTime) + public void Update(float time) { float brightness = 1.0f; if (lightSourceParams.BlinkFrequency > 0.0f) { - blinkTimer = (blinkTimer + deltaTime * lightSourceParams.BlinkFrequency) % 1.0f; + float blinkTimer = (time * lightSourceParams.BlinkFrequency) % 1.0f; if (blinkTimer > 0.5f) { CurrentBrightness = 0.0f; @@ -500,14 +498,13 @@ namespace Barotrauma.Lights } if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) { - pulseState = (pulseState + deltaTime * lightSourceParams.PulseFrequency) % 1.0f; + float pulseState = (time * lightSourceParams.PulseFrequency) % 1.0f; //oscillate between 0-1 brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; } - if (lightSourceParams.Flicker > 0.0f) + if (lightSourceParams.Flicker > 0.0f && lightSourceParams.FlickerSpeed > 0.0f) { - flickerState += deltaTime * lightSourceParams.FlickerSpeed; - flickerState %= 255; + float flickerState = (time * lightSourceParams.FlickerSpeed) % 255; brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; } CurrentBrightness = brightness; @@ -518,19 +515,25 @@ namespace Barotrauma.Lights /// private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { - var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); + var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } - chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); - - NeedsHullCheck = true; + chList.List.Clear(); + foreach (var convexHull in fullChList.List) + { + if (!convexHull.Enabled) { continue; } + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + chList.List.Add(convexHull); + } + chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); + NeedsHullCheck = false; } /// /// Recheck which convex hulls are in range (if needed), /// and check if we need to recalculate vertices due to changes in the convex hulls /// - private void CheckHullsInRange() + private void CheckConvexHullsInRange() { foreach (Submarine sub in Submarine.Loaded) { @@ -543,21 +546,13 @@ namespace Barotrauma.Lights private void CheckHullsInRange(Submarine sub) { //find the list of convexhulls that belong to the sub - ConvexHullList chList = null; - foreach (var ch in hullsInRange) - { - if (ch.Submarine == sub) - { - chList = ch; - break; - } - } - + ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub); + //not found -> create one if (chList == null) { chList = new ConvexHullList(sub); - hullsInRange.Add(chList); + convexHullsInRange.Add(chList); NeedsRecalculation = true; } @@ -649,6 +644,10 @@ namespace Barotrauma.Lights } } + private static readonly List visibleSegments = new List(); + private static readonly List points = new List(); + private static readonly List output = new List(); + private static readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; private List FindRaycastHits() { if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } @@ -656,12 +655,17 @@ namespace Barotrauma.Lights Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - var hulls = new List(); - foreach (ConvexHullList chList in hullsInRange) + visibleSegments.Clear(); + foreach (ConvexHullList chList in convexHullsInRange) { foreach (ConvexHull hull in chList.List) { - if (!chList.IsHidden.Contains(hull)) { hulls.Add(hull); } + if (!chList.IsHidden.Contains(hull)) + { + //find convexhull segments that are close enough and facing towards the light source + hull.RefreshWorldPositions(); + hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); + } } foreach (ConvexHull hull in chList.List) { @@ -669,23 +673,13 @@ namespace Barotrauma.Lights } } - float bounds = TextureRange; - //find convexhull segments that are close enough and facing towards the light source - List visibleSegments = new List(); - List points = new List(); - foreach (ConvexHull hull in hulls) - { - hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); - } - //add a square-shaped boundary to make sure we've got something to construct the triangles from //even if there aren't enough hull segments around the light source //(might be more effective to calculate if we actually need these extra points) Vector2 drawOffset = Vector2.Zero; - float boundsExtended = bounds; + float boundsExtended = TextureRange; if (OverrideLightTexture != null) { float cosAngle = (float)Math.Cos(Rotation); @@ -709,12 +703,12 @@ namespace Barotrauma.Lights drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; } - var boundaryCorners = new SegmentPoint[] { - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null) - }; + Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); + Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); + boundaryCorners[0] = new SegmentPoint(boundsMax, null); + boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null); + boundaryCorners[2] = new SegmentPoint(boundsMin, null); + boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null); for (int i = 0; i < 4; i++) { @@ -798,6 +792,7 @@ namespace Barotrauma.Lights } } + points.Clear(); //remove segments that fall out of bounds for (int i = 0; i < visibleSegments.Count; i++) { @@ -817,7 +812,18 @@ namespace Barotrauma.Lights } } - visibleSegments = visibleSegments.OrderBy(s => MathUtils.LineToPointDistanceSquared(s.Start.WorldPos, s.End.WorldPos, drawPos)).ToList(); + //remove points that are very close to each other + for (int i = 0; i < points.Count; i++) + { + for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) + { + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } + } + } var compareCCW = new CompareSegmentPointCW(drawPos); try @@ -833,23 +839,12 @@ namespace Barotrauma.Lights } DebugConsole.ThrowError(sb.ToString(), e); } + + visibleSegments.Sort((s1, s2) => + MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) + .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); - List output = new List(); - //List> preOutput = new List>(); - - //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) - { - for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) - { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) - { - points.RemoveAt(j); - } - } - } - + output.Clear(); foreach (SegmentPoint p in points) { Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); @@ -857,10 +852,10 @@ namespace Barotrauma.Lights //do two slightly offset raycasts to hit the segment itself and whatever's behind it var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + if (intersection1.index < 0) { return null; } var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); + if (intersection2.index < 0) { return null; } - if (intersection1.index < 0) return null; - if (intersection2.index < 0) return null; Segment seg1 = visibleSegments[intersection1.index]; Segment seg2 = visibleSegments[intersection2.index]; @@ -872,7 +867,7 @@ namespace Barotrauma.Lights //hit at the current segmentpoint -> place the segmentpoint into the list output.Add(p.WorldPos); - foreach (ConvexHullList hullList in hullsInRange) + foreach (ConvexHullList hullList in convexHullsInRange) { hullList.IsHidden.Remove(p.ConvexHull); hullList.IsHidden.Remove(seg1.ConvexHull); @@ -886,7 +881,7 @@ namespace Barotrauma.Lights output.Add(isPoint1 ? p.WorldPos : intersection1.pos); output.Add(isPoint2 ? p.WorldPos : intersection2.pos); - foreach (ConvexHullList hullList in hullsInRange) + foreach (ConvexHullList hullList in convexHullsInRange) { hullList.IsHidden.Remove(p.ConvexHull); hullList.IsHidden.Remove(seg1.ConvexHull); @@ -914,7 +909,7 @@ namespace Barotrauma.Lights return output; } - private (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) + private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) { Vector2? closestIntersection = null; int segment = -1; @@ -939,13 +934,13 @@ namespace Barotrauma.Lights //same for the x-axis if (s.Start.WorldPos.X > s.End.WorldPos.X) { - if (s.Start.WorldPos.X < minX) continue; - if (s.End.WorldPos.X > maxX) continue; + if (s.Start.WorldPos.X < minX) { continue; } + if (s.End.WorldPos.X > maxX) { continue; } } else { - if (s.End.WorldPos.X < minX) continue; - if (s.Start.WorldPos.X > maxX) continue; + if (s.End.WorldPos.X < minX) { continue; } + if (s.Start.WorldPos.X > maxX) { continue; } } bool intersects; @@ -1338,7 +1333,7 @@ namespace Barotrauma.Lights return; } - CheckHullsInRange(); + CheckConvexHullsInRange(); if (NeedsRecalculation && allowRecalculation) { @@ -1390,7 +1385,7 @@ namespace Barotrauma.Lights public void Reset() { - hullsInRange.Clear(); + convexHullsInRange.Clear(); diffToSub.Clear(); NeedsHullCheck = true; NeedsRecalculation = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 573c648c4..2170143c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -70,9 +70,9 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted || !(entity is Item) || !entity.IsMouseOn(position)) { continue; } + if (entity == this|| entity is not Item || !entity.IsMouseOn(position)) { continue; } if (((Item)entity).GetComponent() == null) { continue; } if (linkedTo.Contains(entity)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 12795d4e6..18f4b34a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -69,7 +69,7 @@ namespace Barotrauma private (Rectangle targetArea, RichString tip)? tooltip; - private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + private SubmarineInfo.PendingSubInfo pendingSubInfo; private RichString beaconStationActiveText, beaconStationInactiveText; @@ -213,11 +213,17 @@ namespace Barotrauma currLocationIndicatorPos = CurrentLocation.MapPosition; } - RemoveFogOfWar(newLocation); + if (newLocation.Visited) + { + RemoveFogOfWar(newLocation); + } } + partial void RemoveFogOfWarProjSpecific(Location location) => RemoveFogOfWar(location); + private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { + if (mapTiles == null) { return; } if (location == null) { return; } var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); @@ -453,6 +459,7 @@ namespace Barotrauma }; new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => { + if (location.Reputation == null) { return; } RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); }); @@ -681,6 +688,7 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); Discover(CurrentLocation); + Visit(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) @@ -695,12 +703,6 @@ namespace Barotrauma } } - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) && PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation != null) - { - int distance = DistanceToClosestLocationWithOutpost(HighlightedLocation, out Location foundLocation); - DebugConsole.NewMessage($"Distance to closest outpost from {HighlightedLocation.Name} to {foundLocation?.Name} is {distance}"); - } - if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null) { SelectLocation(-1); @@ -729,6 +731,8 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + float missionIconScale = generationParams.MissionIcon != null ? 18.0f / generationParams.MissionIcon.SourceRect.Width : 1.0f; + Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); @@ -807,10 +811,22 @@ namespace Barotrauma drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; + if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any()) + { + Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y); + generationParams.MissionIcon.Draw(spriteBatch, + offScreenMissionIconPos, + generationParams.IndicatorColor, scale: missionIconScale * zoom); + GUI.Arrow.Draw(spriteBatch, + offScreenMissionIconPos + Vector2.UnitX * generationParams.MissionIcon.size.X * missionIconScale * zoom, + generationParams.IndicatorColor, MathHelper.PiOver2, scale: 0.5f); + } + + if (!rect.Intersects(drawRect)) { continue; } Color color = location.Type.SpriteColor; - if (!location.Discovered) { color = Color.White; } + if (!location.Visited) { color = Color.White; } if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null) { color *= 0.5f; @@ -890,7 +906,6 @@ namespace Barotrauma location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1])) { Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; - float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) { @@ -908,10 +923,17 @@ namespace Barotrauma Vector2 dPos = pos; if (location == HighlightedLocation) { - dPos.Y += 48; - GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: "+(location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + dPos.Y -= 80; + GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawString(spriteBatch, dPos + new Vector2(15, 50), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); dPos.Y += 48; + + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " + + GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + + } } dPos.Y += 48; GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); @@ -1045,7 +1067,7 @@ namespace Barotrauma } float a = 1.0f; - if (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered) + if (!connection.Locations[0].Visited && !connection.Locations[1].Visited) { if (IsInFogOfWar(connection.Locations[0])) { @@ -1089,39 +1111,8 @@ namespace Barotrauma if (connection.LevelData.HasHuntingGrounds) { iconCount++; } if (connection.Locked) { iconCount++; } string tooltip = null; - float subCrushDepth = Level.DefaultRealWorldCrushDepth; - var currentOrPendingSub = SubmarineSelection.CurrentOrPendingSubmarine(); - if (Submarine.MainSub != null && Submarine.MainSub.Info == currentOrPendingSub) - { - subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; - } - else if (currentOrPendingSub != null) - { - if (pendingSubInfo.pendingSub != currentOrPendingSub) - { - // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time - pendingSubInfo = (currentOrPendingSub, currentOrPendingSub.GetRealWorldCrushDepth()); - } - subCrushDepth = pendingSubInfo.realWorldCrushDepth; - } - if (GameMain.GameSession?.Campaign?.UpgradeManager != null) - { - var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth".ToIdentifier()); - if (hullUpgradePrefab != null) - { - int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); - int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); - if (pendingLevel > currentLevel) - { - string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); - if (!string.IsNullOrEmpty(updateValueStr)) - { - subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr); - } - } - } - } + float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo); string crushDepthWarningIconStyle = null; if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) { @@ -1277,6 +1268,14 @@ namespace Barotrauma } } + /// + /// Resets and forces crush depth to be calculated again for icon displaying purposes + /// + public void ResetPendingSub() + { + pendingSubInfo = new SubmarineInfo.PendingSubInfo(); + } + partial void RemoveProjSpecific() { noiseOverlay?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 99c42266e..151e15a14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -36,7 +36,7 @@ namespace Barotrauma public static List CopiedList = new List(); - private static List highlightedList = new List(); + private static List highlightedInEditorList = new List(); private static float highlightTimer; @@ -131,10 +131,7 @@ namespace Barotrauma return; } - foreach (MapEntity e in mapEntityList) - { - e.isHighlighted = false; - } + ClearHighlightedEntities(); if (DisableSelect) { @@ -262,11 +259,10 @@ namespace Barotrauma if (i == 0) highLightedEntity = e; } } - UpdateHighlighting(highlightedEntities); } - if (highLightedEntity != null) highLightedEntity.isHighlighted = true; + if (highLightedEntity != null) { highLightedEntity.IsHighlighted = true; } } if (GUI.KeyboardDispatcher.Subscriber == null) @@ -288,7 +284,6 @@ namespace Barotrauma if (startMovingPos != Vector2.Zero) { Item targetContainer = GetPotentialContainer(position, SelectedList); - if (targetContainer != null) { targetContainer.IsHighlighted = true; } if (PlayerInput.PrimaryMouseButtonReleased()) @@ -610,10 +605,10 @@ namespace Barotrauma if (highlightedListBox != null) { if (GUI.MouseOn == highlightedListBox || highlightedListBox.IsParentOf(GUI.MouseOn)) return; - if (highlightedEntities.SequenceEqual(highlightedList)) return; + if (highlightedEntities.SequenceEqual(highlightedInEditorList)) return; } - highlightedList = highlightedEntities; + highlightedInEditorList = highlightedEntities; highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { @@ -1096,7 +1091,7 @@ namespace Barotrauma private void UpdateResizing(Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; @@ -1197,7 +1192,7 @@ namespace Barotrauma private void DrawResizing(SpriteBatch spriteBatch, Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index a08893601..f15259990 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -89,7 +89,11 @@ namespace Barotrauma CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) + public void CreateSpecsWindow(GUIListBox parent, GUIFont font, + bool includeTitle = true, + bool includeClass = true, + bool includeDescription = false, + bool includeCrushDepth = false) { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; @@ -155,6 +159,22 @@ namespace Barotrauma { CanBeFocused = false }; cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height); + if (includeCrushDepth) + { + var crushDepthText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("CrushDepth"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crushDepthText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.GetWithVariable("meterformat", "[meters]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetSubCrushDepth())), + textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + crushDepthText.RectTransform.MinSize = new Point(0, crushDepthText.Children.First().Rect.Height); + } + if (RecommendedCrewSizeMax > 0) { var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), @@ -227,5 +247,57 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren().Where(c => c != submarineNameText && c != descBlock)); parent.ForceLayoutRecalculation(); } + + public readonly record struct PendingSubInfo(SubmarineInfo PendingSub = null, bool StructuresDefineRealWorldCrushDepth = false, float RealWorldCrushDepth = Level.DefaultRealWorldCrushDepth); + + private float GetSubCrushDepth() + { + var pendingSubInfo = new PendingSubInfo(); + return GetSubCrushDepth(this, ref pendingSubInfo); + } + + public static float GetSubCrushDepth(SubmarineInfo subInfo, ref PendingSubInfo pendingSubInfo) + { + float subCrushDepth = Level.DefaultRealWorldCrushDepth; + if (Submarine.MainSub != null && Submarine.MainSub.Info == subInfo) + { + subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; + } + else if (subInfo != null) + { + if (pendingSubInfo.PendingSub != subInfo) + { + // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time + pendingSubInfo = new PendingSubInfo(subInfo, subInfo.IsCrushDepthDefinedInStructures(out float realWorldCrushDepth), realWorldCrushDepth); + } + subCrushDepth = pendingSubInfo.RealWorldCrushDepth; + } + if (GameMain.GameSession?.Campaign?.UpgradeManager != null && UpgradePrefab.Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab) + { + int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), info: subInfo); + // If there is a sub switch pending, unless its structures have crush depth defined in their elements, + // calculate the value based on the default crush depth and pending upgrade level + int currentLevel = 0; + if (pendingSubInfo.PendingSub is null || pendingSubInfo.StructuresDefineRealWorldCrushDepth) + { + currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevelForSub(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), subInfo); + } + if (pendingLevel > currentLevel) + { + string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); + if (!string.IsNullOrEmpty(updateValueStr)) + { + if (currentLevel > 0) + { + // If the current level is greater than 0, reset the crush depth value back to the base value before calculating the upgrade + int upgradePercentage = UpgradePrefab.ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings: true); + subCrushDepth /= (1f + (upgradePercentage / 100f * currentLevel)); + } + subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel, updateValueStr); + } + } + } + return subCrushDepth; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e547ec854..8491dd736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -4,26 +4,29 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - class SubmarinePreview : IDisposable + sealed class SubmarinePreview : IDisposable { - private SpriteRecorder spriteRecorder; private readonly SubmarineInfo submarineInfo; + + private SpriteRecorder spriteRecorder; private Camera camera; private Task loadTask; + private (Vector2 Min, Vector2 Max) bounds; + private volatile bool isDisposed; private GUIFrame previewFrame; - private class HullCollection + private sealed class HullCollection { public readonly List Rects; public readonly LocalizedString Name; @@ -42,7 +45,7 @@ namespace Barotrauma } } - private struct Door + private readonly struct Door { public readonly Rectangle Rect; @@ -113,7 +116,7 @@ namespace Barotrauma bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && - (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + (PlayerInput.MidButtonHeld() || PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; moveSpeed.X = -moveSpeed.X; @@ -150,7 +153,9 @@ namespace Barotrauma ScrollBarVisible = false, Spacing = GUI.IntScale(5) }; - subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); + subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, + includeTitle: false, + includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { @@ -186,7 +191,22 @@ namespace Barotrauma }); recalculateSpecsContainerHeight(); - GeneratePreviewMeshes(); + TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => + { + if (isDisposed) { return; } + // Reset the camera's position on the main thread, + // because the Camera class is not thread-safe and + // it's possible for its state to not get updated + // properly if done within a task + camera.Position = (bounds.Min + bounds.Max) * (0.5f, -0.5f); + Vector2 span2d = bounds.Max - bounds.Min; + Vector2 scaledSpan2d = span2d / camera.Resolution.ToVector2(); + float scaledSpan = Math.Max(scaledSpan2d.X, scaledSpan2d.Y); + camera.MinZoom = Math.Min(0.1f, 0.4f / scaledSpan); + camera.Zoom = 0.7f / scaledSpan; + camera.StopMovement(); + camera.UpdateTransform(interpolate: false, updateListener: false); + }); } public static void AddToGUIUpdateList() @@ -207,6 +227,7 @@ namespace Barotrauma spriteRecorder.Begin(SpriteSortMode.BackToFront); HashSet toIgnore = new HashSet(); + HashSet wires = new HashSet(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { @@ -221,7 +242,7 @@ namespace Barotrauma ExtractItemContainerIds(component, toIgnore); break; case "connectionpanel": - ExtractConnectionPanelLinks(component, toIgnore); + ExtractConnectionPanelLinks(component, wires); break; } } @@ -231,20 +252,25 @@ namespace Barotrauma await Task.Yield(); } + var wireNodes = new List(); + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } switch (subElement.Name.LocalName.ToLowerInvariant()) { + case "structure": case "item": - if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) + var id = subElement.GetAttributeInt("ID", 0); + if (wires.Contains(id)) + { + wireNodes.Add(subElement); + } + else if (!toIgnore.Contains(id)) { BakeMapEntity(subElement); } break; - case "structure": - BakeMapEntity(subElement); - break; case "hull": Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) @@ -261,15 +287,14 @@ namespace Barotrauma if (isDisposed) { return; } await Task.Yield(); } - spriteRecorder.End(); - camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; - float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; - camera.Zoom = 0.8f / scaledSpan; - camera.StopMovement(); + bounds = (spriteRecorder.Min, spriteRecorder.Max); + wireNodes.ForEach(BakeWireNodes); + + spriteRecorder.End(); } - private void ExtractItemContainerIds(XElement component, HashSet ids) + private static void ExtractItemContainerIds(XElement component, HashSet ids) { string containedString = component.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); @@ -283,7 +308,7 @@ namespace Barotrauma } } - private void ExtractConnectionPanelLinks(XElement component, HashSet ids) + private static void ExtractConnectionPanelLinks(XElement component, HashSet ids) { var pins = component.Elements("input").Concat(component.Elements("output")); foreach (var pin in pins) @@ -297,6 +322,39 @@ namespace Barotrauma } } + private void BakeWireNodes(XElement element) + { + var prefabIdentifier = element.GetAttributeIdentifier("identifier", ""); + if (prefabIdentifier.IsEmpty) { return; } + if (!ItemPrefab.Prefabs.TryGet(prefabIdentifier, out var prefab)) { return; } + + var prefabWireComponentElement = prefab.ConfigElement.GetChildElement("wire"); + if (prefabWireComponentElement is null) { return; } + + var wireComponent = element.GetChildElement("wire"); + if (wireComponent is null) { return; } + + var color = element.GetAttributeColor("spritecolor") ?? Color.White; + + var nodes = Wire.ExtractNodes(wireComponent).ToImmutableArray(); + var wireSprite = Wire.ExtractWireSprite(prefab.ConfigElement); + + var useSpriteDepth = element.GetAttributeBool("usespritedepth", false); + var depth = + useSpriteDepth + ? element.GetAttributeFloat("spritedepth", 1.0f) + : wireSprite.Depth; + + var width = prefabWireComponentElement.GetAttributeFloat("width", 0.3f); + + for (int i = 0; i < nodes.Length - 1; i++) + { + var line = (Start: nodes[i], End: nodes[i + 1]); + var wireSegment = new Wire.WireSection(line.Start, line.End); + wireSegment.Draw(spriteRecorder, wireSprite, color, Vector2.Zero, depth, width); + } + } + private void BakeMapEntity(XElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); @@ -313,27 +371,27 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = null; - if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && - ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + MapEntityPrefab prefab; + if (element.NameAsIdentifier() == "item" + && ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) { prefab = ip; } else { - prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + prefab = MapEntityPrefab.FindByIdentifier(identifier); } if (prefab == null) { return; } - var texture = prefab.Sprite.Texture; - var srcRect = prefab.Sprite.SourceRect; + flippedX &= prefab.CanSpriteFlipX; + flippedY &= prefab.CanSpriteFlipY; SpriteEffects spriteEffects = SpriteEffects.None; - if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) + if (flippedX) { spriteEffects |= SpriteEffects.FlipHorizontally; } - if (flippedY && ((prefab as ItemPrefab)?.CanSpriteFlipY ?? true)) + if (flippedY) { spriteEffects |= SpriteEffects.FlipVertically; } @@ -419,8 +477,8 @@ namespace Barotrauma { float offsetState = 0f; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + if (flippedX) { offset.X = -offset.X; } + if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteRecorder, new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, @@ -451,8 +509,8 @@ namespace Barotrauma float rotationState = 0f; float offsetState = 0f; float rot = decorativeSprite.GetRotation(ref rotationState, 0f); Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + if (flippedX) { offset.X = -offset.X; } + if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); @@ -472,6 +530,7 @@ namespace Barotrauma { overrideSprite = false; + float relativeScale = scale / prefab.Scale; foreach (var subElement in prefab.ConfigElement.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -498,7 +557,6 @@ namespace Barotrauma relativeBarrelPos, MathHelper.ToRadians(rotation)); - float relativeScale = scale / prefab.Scale; Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); drawPos.Y = -drawPos.Y; @@ -516,20 +574,22 @@ namespace Barotrauma break; case "door": - doors.Add(new Door(rect)); + var scaledRect = rect with { Size = (rect.Size.ToVector2() * relativeScale).ToPoint() }; + + doors.Add(new Door(scaledRect)); var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); if (doorSpriteElem != null) { - string texturePath = doorSpriteElem.GetAttributeString("texture", ""); - Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); + string texturePath = doorSpriteElem.GetAttributeStringUnrestricted("texture", ""); + Vector2 pos = scaledRect.Location.ToVector2() * new Vector2(1f, -1f); if (subElement.GetAttributeBool("horizontal", false)) { - pos.Y += (float)rect.Height * 0.5f; + pos.Y += (float)scaledRect.Height * 0.5f; } else { - pos.X += (float)rect.Width * 0.5f; + pos.X += (float)scaledRect.Width * 0.5f; } Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); spriteRecorder.Draw(doorSprite.Texture, pos, @@ -555,7 +615,7 @@ namespace Barotrauma } } - public void ParseUpgrades(XElement prefabConfigElement, ref float scale) + private void ParseUpgrades(XElement prefabConfigElement, ref float scale) { foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 72eba95b7..cded39e96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -39,7 +39,7 @@ namespace Barotrauma { Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } - if (isObstructed) + if (!IsTraversable) { clr = Color.Black; } @@ -84,7 +84,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); + (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { @@ -175,9 +175,9 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.Space)) { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in HighlightedEntities) { - if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } + if (e is not WayPoint || e == this) { continue; } if (linkedTo.Contains(e)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 68ba8fbae..ad412a92b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Networking string name, Either addressOrAccountId, string reason, - DateTime? expiration) + Option expiration) { this.Name = name; this.AddressOrAccountId = addressOrAccountId; @@ -94,8 +94,9 @@ namespace Barotrauma.Networking topArea.ForceLayoutRecalculation(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), - bannedPlayer.ExpirationTime == null ? - TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), + bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime) + ? TextManager.GetWithVariable("BanExpires", "[time]", expirationTime.ToLocalUserString()) + : TextManager.Get("BanPermanent"), font: GUIStyle.SmallFont); LocalizedString reason = TextManager.GetServerMessage(bannedPlayer.Reason).Fallback(bannedPlayer.Reason); @@ -149,11 +150,11 @@ namespace Barotrauma.Networking bool includesExpiration = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - DateTime? expiration = null; + Option expiration = Option.None(); if (includesExpiration) { double hoursFromNow = incMsg.ReadDouble(); - expiration = DateTime.Now + TimeSpan.FromHours(hoursFromNow); + expiration = Option.Some(SerializableDateTime.LocalNow + TimeSpan.FromHours(hoursFromNow)); } string reason = incMsg.ReadString(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 4ae286687..cab954d3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -26,8 +26,8 @@ namespace Barotrauma.Networking if (type != ChatMessageType.Order) { changeType = (PlayerConnectionChangeType)msg.ReadByte(); - txt = msg.ReadString(); } + txt = msg.ReadString(); string senderName = msg.ReadString(); Character senderCharacter = null; @@ -87,11 +87,6 @@ namespace Barotrauma.Networking targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value; } - txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, - givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, - orderOption: orderOption, - isNewOrder: orderMessageInfo.IsNewOrder); - if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { Order order = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1809817cb..f86c9cd48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -332,7 +332,6 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -414,7 +413,6 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 58c9903e0..d31ea903b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -326,7 +326,7 @@ namespace Barotrauma.Networking return serverEndpoint switch { LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), - SteamP2PEndpoint _ when ownerKey is Some { Value: var key } => new SteamP2POwnerPeer(callbacks, key), + SteamP2PEndpoint _ when ownerKey.TryUnwrap(out var key) => new SteamP2POwnerPeer(callbacks, key), SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), _ => throw new ArgumentOutOfRangeException() }; @@ -522,7 +522,7 @@ namespace Barotrauma.Networking if (GameStarted && Screen.Selected == GameMain.GameScreen) { - EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); + EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned; RespawnManager?.Update(deltaTime); @@ -988,7 +988,7 @@ namespace Barotrauma.Networking GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); - CampaignMode.StartRoundCancellationToken?.Cancel(); + GameMain.GameSession?.Campaign?.CancelStartRound(); if (SteamManager.IsInitialized) { @@ -1761,7 +1761,7 @@ namespace Barotrauma.Networking { string subName = inc.ReadString(); string subHash = inc.ReadString(); - byte subClass = inc.ReadByte(); + SubmarineClass subClass = (SubmarineClass)inc.ReadByte(); bool isShuttle = inc.ReadBoolean(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); @@ -1770,7 +1770,7 @@ namespace Barotrauma.Networking { matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false) { - SubmarineClass = (SubmarineClass)subClass + SubmarineClass = subClass }; if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); } } @@ -2003,10 +2003,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); GameMain.NetLobbyScreen.SetMissionType(missionType); - if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); + GameMain.NetLobbyScreen.SelectMode(modeIndex); if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); + if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); } } if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) @@ -2405,7 +2405,9 @@ namespace Barotrauma.Networking var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); + var existingSubs = SubmarineInfo.SavedSubmarines + .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash) + .ToList(); foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); @@ -2464,12 +2466,13 @@ namespace Barotrauma.Networking } // Replace a submarine dud with the downloaded version - SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => + s.Name == newSub.Name + && s.MD5Hash == newSub.MD5Hash); if (existingServerSub != null) { int existingIndex = ServerSubmarines.IndexOf(existingServerSub); - ServerSubmarines.RemoveAt(existingIndex); - ServerSubmarines.Insert(existingIndex, newSub); + ServerSubmarines[existingIndex] = newSub; existingServerSub.Dispose(); } @@ -2620,7 +2623,13 @@ namespace Barotrauma.Networking using (var segmentTable = SegmentTableWriter.StartWriting(msg)) { segmentTable.StartNewSegment(ClientNetSegment.Vote); - Voting.ClientWrite(msg, voteType, data); + bool succeeded = Voting.ClientWrite(msg, voteType, data); + if (!succeeded) + { + throw new Exception( + $"Failed to write vote of type {voteType}: " + + $"data was of invalid type {data?.GetType().Name ?? "NULL"}"); + } } ClientPeer.Send(msg, DeliveryMethod.Reliable); @@ -2790,7 +2799,6 @@ namespace Barotrauma.Networking /// public void RequestSelectMode(int modeIndex) { - if (!HasPermission(ClientPermissions.SelectMode)) return; if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren) { DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); @@ -2844,13 +2852,14 @@ namespace Barotrauma.Networking /// /// Tell the server to end the round (permission required) /// - public void RequestRoundEnd(bool save) + public void RequestRoundEnd(bool save, bool quitCampaign = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); msg.WriteBoolean(true); //indicates round end msg.WriteBoolean(save); + msg.WriteBoolean(quitCampaign); ClientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 54a932e0b..c51b66457 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -82,6 +82,11 @@ namespace Barotrauma.Networking Initialization = ConnectionInitialization.SteamTicketAndVersion }; + if (steamAuthTicket is { Canceled: true }) + { + throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled."); + } + ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket { Name = GameMain.Client.Name, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 54c4d7ca2..5c77d37c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Networking var packet = INetSerializableStruct.Read(inc); - packet.SteamAuthTicket.TryUnwrap(out byte[] ticket); + packet.SteamAuthTicket.TryUnwrap(out var ticket); Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index adbf863df..0c778b22c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -1,5 +1,6 @@ #nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -69,6 +70,9 @@ namespace Barotrauma.Networking [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); @@ -281,7 +285,7 @@ namespace Barotrauma.Networking // ----------------------------------------------------------------------------- - float elementHeight = 0.075f; + const float elementHeight = 0.075f; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); @@ -294,6 +298,11 @@ namespace Barotrauma.Networking serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Language")); + new GUITextBlock(new RectTransform(Vector2.One, languageLabel.RectTransform), + ServerLanguageOptions.Options.FirstOrNull(o => o.Identifier == Language)?.Label ?? TextManager.Get("Unknown"), + textAlignment: Alignment.Right); + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), @@ -363,7 +372,7 @@ namespace Barotrauma.Networking packageText.Selected = true; } //workshop download link found - else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + else if (package.Id.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId) { packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); } @@ -417,6 +426,7 @@ namespace Barotrauma.Networking GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c6a97de44..339e6051b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -78,7 +79,7 @@ namespace Barotrauma.Networking } } private Dictionary tempMonsterEnabled; - + partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); @@ -367,6 +368,15 @@ namespace Barotrauma.Networking //*********************************************** + // Language + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); + //changing server visibility on the fly is not supported in dedicated servers if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 7698c4b66..d23386f33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -116,10 +116,13 @@ namespace Barotrauma.Networking bool spectating = Character.Controlled == null; float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent radio = null; - var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; + var messageType = + !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? + ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; + client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier); @@ -131,7 +134,6 @@ namespace Barotrauma.Networking } else { - client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); } client.VoipSound.UseMuffleFilter = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index f07d6f7a9..6e7a4df7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -13,13 +13,11 @@ namespace Barotrauma { public SubmarineInfo SubmarineInfo { get; set; } public bool TransferItems { get; set; } - public int DeliveryFee { get; set; } - public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems) { SubmarineInfo = submarineInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; } } @@ -98,16 +96,15 @@ namespace Barotrauma foreach (GUIComponent comp in listBox.Content.Children) { if (comp.UserData != userData) { continue; } - if (!(comp.FindChild("votes") is GUITextBlock voteText)) + if (comp.FindChild("votes") is not GUITextBlock voteText) { - voteText = new GUITextBlock(new RectTransform(new Point(30, comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), - "", textAlignment: Alignment.CenterRight) + voteText = new GUITextBlock(new RectTransform(new Point(GUI.IntScale(30), comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), + "", textAlignment: Alignment.Center) { Padding = Vector4.Zero, UserData = "votes" }; } - voteText.Text = votes == 0 ? "" : votes.ToString(); } } @@ -129,64 +126,72 @@ namespace Barotrauma UpdateVoteTexts(connectedClients, VoteType.Sub); } - public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) + /// + /// Returns true if the given data is valid for the given vote type, + /// returns false otherwise. If it returns false, the message must + /// be discarded or reset by the caller, as it is now malformed :) + /// + public bool ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.WriteByte((byte)voteType); switch (voteType) { case VoteType.Sub: - if (!(data is SubmarineInfo sub)) { return; } + if (data is not SubmarineInfo sub) { return false; } msg.WriteInt32(sub.EqualityCheckVal); - if (sub.EqualityCheckVal == 0) + if (sub.EqualityCheckVal <= 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for msg.WriteString(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: - if (!(data is GameModePreset gameMode)) { return; } + if (data is not GameModePreset gameMode) { return false; } msg.WriteIdentifier(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool endRound) { return false; } + msg.WriteBoolean(endRound); break; case VoteType.Kick: - if (!(data is Client votedClient)) { return; } + if (data is not Client votedClient) { return false; } msg.WriteByte(votedClient.SessionId); break; case VoteType.StartRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool startRound) { return false; } + msg.WriteBoolean(startRound); break; case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is (SubmarineInfo voteSub, bool transferItems)) - { - //initiate sub vote - msg.WriteBoolean(true); - msg.WriteString(voteSub.Name); - msg.WriteBoolean(transferItems); - } - else + switch (data) { - // vote - if (!(data is int)) { return; } - msg.WriteBoolean(false); - msg.WriteInt32((int)data); + case (SubmarineInfo voteSub, bool transferItems): + //initiate sub vote + msg.WriteBoolean(true); + msg.WriteString(voteSub.Name); + msg.WriteBoolean(transferItems); + break; + case int vote: + // vote + msg.WriteBoolean(false); + msg.WriteInt32(vote); + break; + default: + return false; } break; case VoteType.TransferMoney: - if (!(data is int)) { return; } + if (data is not int money) { return false; } msg.WriteBoolean(false); //not initiating a vote - msg.WriteInt32((int)data); + msg.WriteInt32(money); break; } msg.WritePadBits(); + return true; } public void ClientRead(IReadMessage inc) @@ -325,13 +330,12 @@ namespace Barotrauma string subName2 = inc.ReadString(); var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? 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); + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems); break; } @@ -343,13 +347,13 @@ namespace Barotrauma { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 23c90ff23..07ebfee4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -11,15 +11,13 @@ namespace Barotrauma public enum MouseButton { None = -1, - LeftMouse = 0, - RightMouse = 1, + PrimaryMouse = 0, + SecondaryMouse = 1, MiddleMouse = 2, MouseButton4 = 3, MouseButton5 = 4, MouseWheelUp = 5, - MouseWheelDown = 6, - PrimaryMouse, - SecondaryMouse + MouseWheelDown = 6 } public class KeyOrMouse @@ -65,10 +63,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonHeld(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonHeld(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonHeld(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonHeld(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonHeld(); case MouseButton.MouseButton4: @@ -95,10 +89,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonClicked(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonClicked(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonClicked(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonClicked(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonClicked(); case MouseButton.MouseButton4: @@ -218,11 +208,11 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + return PlayerInput.PrimaryMouseLabel; case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + return PlayerInput.SecondaryMouseLabel; default: - return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + return TextManager.Get($"Input.{MouseButton}"); } } else @@ -270,6 +260,9 @@ namespace Barotrauma } #endif + public static readonly LocalizedString PrimaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Left" : "Right")}Mouse"); + public static readonly LocalizedString SecondaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Right" : "Left")}Mouse"); + public static Vector2 MousePosition { get { return new Vector2(mouseState.Position.X, mouseState.Position.Y); } @@ -317,120 +310,48 @@ namespace Barotrauma } public static bool PrimaryMouseButtonHeld() - { - if (MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool PrimaryMouseButtonDown() - { - if (MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool PrimaryMouseButtonReleased() - { - if (MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool PrimaryMouseButtonClicked() - { - if (MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool SecondaryMouseButtonHeld() - { - if (!MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool SecondaryMouseButtonDown() - { - if (!MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool SecondaryMouseButtonReleased() - { - if (!MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool SecondaryMouseButtonClicked() - { - if (!MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool LeftButtonHeld() { return AllowInput && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonDown() + public static bool PrimaryMouseButtonDown() { return AllowInput && oldMouseState.LeftButton == ButtonState.Released && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonReleased() + public static bool PrimaryMouseButtonReleased() { return AllowInput && mouseState.LeftButton == ButtonState.Released; } - public static bool LeftButtonClicked() + public static bool PrimaryMouseButtonClicked() { return (AllowInput && oldMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released); } - public static bool RightButtonHeld() + public static bool SecondaryMouseButtonHeld() { return AllowInput && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonDown() + public static bool SecondaryMouseButtonDown() { return AllowInput && oldMouseState.RightButton == ButtonState.Released && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonReleased() + public static bool SecondaryMouseButtonReleased() { return AllowInput && mouseState.RightButton == ButtonState.Released; } - public static bool RightButtonClicked() + public static bool SecondaryMouseButtonClicked() { return (AllowInput && oldMouseState.RightButton == ButtonState.Pressed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index 56dce764f..fc8859439 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -23,15 +23,12 @@ namespace Barotrauma ScrollBarEnabled = false, AllowMouseWheelScroll = false }; - new GUIButton(new RectTransform(new Vector2(0.1f), creditsPlayer.RectTransform, Anchor.BottomRight, maxSize: new Point(300, 50)) { AbsoluteOffset = new Point(GUI.IntScale(20)) }, - TextManager.Get("close")) + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - creditsPlayer.Scroll = 1.0f; - return true; - } + creditsPlayer.Scroll = 1.0f; + return true; }; + cam = new Camera(); } @@ -44,7 +41,16 @@ namespace Barotrauma } creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); + UnlockAchievement("campaigncompleted"); + UnlockAchievement( + GameMain.GameSession is { Campaign.Settings.RadiationEnabled: true } ? + "campaigncompleted_radiationenabled" : + "campaigncompleted_radiationdisabled"); + + static void UnlockAchievement(string id) + { + SteamAchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); + } } public override void Deselect() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 60dc7f6ea..bb8f98570 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Barotrauma.IO; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -37,7 +38,6 @@ namespace Barotrauma protected set; } - public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } @@ -58,7 +58,7 @@ namespace Barotrauma var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") { - UserData = saveInfo.FilePath + UserData = saveInfo }; var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), @@ -87,10 +87,9 @@ namespace Barotrauma }; string saveTimeStr = string.Empty; - if (saveInfo.SaveTime > 0) + if (saveInfo.SaveTime.TryUnwrap(out var time)) { - DateTime time = ToolBox.Epoch.ToDateTime(saveInfo.SaveTime); - saveTimeStr = time.ToString(); + saveTimeStr = time.ToLocalUserString(); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) @@ -102,8 +101,29 @@ namespace Barotrauma return saveFrame; } + protected void SortSaveList() + { + saveList.Content.RectTransform.SortChildren((c1, c2) => + { + if (c1.GUIComponent.UserData is not CampaignMode.SaveInfo file1 + || c2.GUIComponent.UserData is not CampaignMode.SaveInfo file2) + { + return 0; + } + + if (!file1.SaveTime.TryUnwrap(out var file1WriteTime) + || !file2.SaveTime.TryUnwrap(out var file2WriteTime)) + { + return 0; + } + + return file2WriteTime.CompareTo(file1WriteTime); + }); + } + public struct CampaignSettingElements { + public SettingValue SelectedPreset; public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; @@ -115,6 +135,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + PresetName = SelectedPreset.GetValue(), TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), @@ -165,9 +186,13 @@ namespace Barotrauma { const float verticalSize = 0.14f; + bool loadingPreset = false; + 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); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); + presetDropdown.Select(0); presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); @@ -175,21 +200,30 @@ namespace Barotrauma { string name = settings.PresetName; presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + + if (settings.PresetName.Equals(prevSettings.PresetName, StringComparison.OrdinalIgnoreCase)) + { + presetDropdown.SelectItem(settings); + } } + var presetValue = new SettingValue( + get: () => presetDropdown.SelectedData is CampaignSettings settings ? settings.PresetName : string.Empty, + set: static _ => { }); // we do not need a way to set this value + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) { Spacing = GUI.IntScale(5) }; SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : - new SettingValue(() => false, b => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : + new SettingValue(static () => false, static _ => { }); + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); 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); + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); ImmutableArray> fundOptions = ImmutableArray.Create( new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), @@ -198,7 +232,7 @@ namespace Barotrauma ); 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); + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); ImmutableArray> difficultyOptions = ImmutableArray.Create( new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), @@ -208,30 +242,38 @@ namespace Barotrauma ); 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 difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, - verticalSize); + verticalSize, + OnValuesChanged); - presetDropdown.OnSelected = (selected, o) => + presetDropdown.OnSelected = (_, o) => { - if (o is CampaignSettings settings) - { - tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); - radiationEnabled.SetValue(settings.RadiationEnabled); - maxMissionCountInput.SetValue(settings.MaxMissionCount); - startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); - startingSetInput.SetValue(settings.StartItemSet); - return true; - } - return false; + if (o is not CampaignSettings settings) { return false; } + + loadingPreset = true; + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + loadingPreset = false; + return true; }; + void OnValuesChanged() + { + if (loadingPreset) { return; } + presetDropdown.Select(0); + } + return new CampaignSettingElements { + SelectedPreset = presetValue, TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, @@ -241,7 +283,7 @@ namespace Barotrauma }; // 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) + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -266,9 +308,11 @@ namespace Barotrauma minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + numberInput.OnValueChanged += _ => onChanged(); + bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } numberInput.IntValue += change; return true; @@ -278,7 +322,7 @@ namespace Barotrauma } static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options) + ImmutableArray> options, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -329,6 +373,8 @@ namespace Barotrauma return true; } + numberInput.OnValueChanged += _ => onChanged(); + void SetValue(int value) { numberInput.IntValue = value; @@ -338,7 +384,7 @@ namespace Barotrauma 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) + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) { 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); @@ -350,6 +396,13 @@ namespace Barotrauma tickBox.Box.IgnoreLayoutGroups = true; tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + tickBox.OnSelected += _ => + { + onChanged(); + return true; + }; + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } @@ -367,5 +420,25 @@ namespace Barotrauma return inputContainer; } } + + public abstract void UpdateLoadMenu(IEnumerable saveFiles = null); + + protected bool DeleteSave(GUIButton button, object obj) + { + if (obj is not CampaignMode.SaveInfo saveInfo) { return false; } + + var header = TextManager.Get("deletedialoglabel"); + var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveInfo.FilePath)); + + EventEditorScreen.AskForConfirmation(header, body, () => + { + SaveUtil.DeleteSave(saveInfo.FilePath); + prevSaveFiles?.RemoveAll(s => s.FilePath == saveInfo.FilePath); + UpdateLoadMenu(prevSaveFiles.ToList()); + return true; + }); + + return true; + } } } \ 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 1a19fc973..c86f3c04e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -192,7 +192,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -220,37 +220,16 @@ namespace Barotrauma CreateSaveElement(saveInfo); } - saveList.Content.RectTransform.SortChildren((c1, c2) => - { - string file1 = c1.GUIComponent.UserData as string; - string file2 = c2.GUIComponent.UserData as string; - DateTime file1WriteTime = DateTime.MinValue; - DateTime file2WriteTime = DateTime.MinValue; - try - { - 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 - { - file2WriteTime = File.GetLastWriteTime(file2); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - return file2WriteTime.CompareTo(file1WriteTime); - }); + SortSaveList(); loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) { OnClicked = (btn, obj) => { - if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } - LoadGame?.Invoke(saveList.SelectedData as string); + if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } + if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } + LoadGame?.Invoke(saveInfo.FilePath); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); return true; }, @@ -264,37 +243,20 @@ namespace Barotrauma }; } + private bool SelectSaveFile(GUIComponent component, object obj) { - string fileName = (string)obj; + if (obj is not CampaignMode.SaveInfo saveInfo) { return true; } + string fileName = saveInfo.FilePath; loadGameButton.Enabled = true; deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; if (deleteMpSaveButton.Visible) { - deleteMpSaveButton.UserData = obj as string; + deleteMpSaveButton.UserData = saveInfo; } return true; } - - private bool DeleteSave(GUIButton button, object obj) - { - string saveFile = obj as string; - if (obj == null) { return false; } - - var header = TextManager.Get("deletedialoglabel"); - var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); - - EventEditorScreen.AskForConfirmation(header, body, () => - { - SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); - UpdateLoadMenu(prevSaveFiles.ToList()); - return true; - }); - - return true; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index c13194e4e..c23b2b190 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -194,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CurrentSettings.InitialMoney; + int initialMoney = CampaignSettings.CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -208,15 +208,15 @@ namespace Barotrauma { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(CurrentSettings, settings => + CreateCustomizeWindow(CampaignSettings.CurrentSettings, settings => { - CampaignSettings prevSettings = CurrentSettings; - CurrentSettings = settings; + CampaignSettings prevSettings = CampaignSettings.CurrentSettings; + CampaignSettings.CurrentSettings = settings; if (prevSettings.InitialMoney != settings.InitialMoney) { object selectedData = subList.SelectedData; UpdateSubList(SubmarineInfo.SavedSubmarines); - if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CampaignSettings.CurrentSettings.InitialMoney) { subList.Select(selectedData); } @@ -375,6 +375,7 @@ namespace Barotrauma { onClosed?.Invoke(elements.CreateSettings()); + GameSettings.SaveCurrentConfig(); return CampaignCustomizeSettings.Close(button, o); }; } @@ -399,7 +400,7 @@ namespace Barotrauma SubmarineInfo selectedSub = null; - if (!(subList.SelectedData is SubmarineInfo)) { return false; } + if (subList.SelectedData is not SubmarineInfo) { return false; } selectedSub = subList.SelectedData as SubmarineInfo; if (selectedSub.SubmarineClass == SubmarineClass.Undefined) @@ -419,7 +420,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = CurrentSettings; + CampaignSettings settings = CampaignSettings.CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -476,7 +477,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { return; } + if (child.UserData is not SubmarineInfo sub) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -487,9 +488,9 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - if (!(obj is SubmarineInfo sub)) { return true; } + if (obj is not SubmarineInfo sub) { return true; } #if !DEBUG - if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -551,7 +552,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignSettings.CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), @@ -563,7 +564,7 @@ namespace Barotrauma #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -573,7 +574,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignSettings.CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); @@ -581,7 +582,7 @@ namespace Barotrauma } } - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -649,46 +650,27 @@ namespace Barotrauma } } - saveList.Content.RectTransform.SortChildren((c1, c2) => - { - string file1 = c1.GUIComponent.UserData as string; - string file2 = c2.GUIComponent.UserData as string; - DateTime file1WriteTime = DateTime.MinValue; - DateTime file2WriteTime = DateTime.MinValue; - try - { - 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 - { - file2WriteTime = File.GetLastWriteTime(file2); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - return file2WriteTime.CompareTo(file1WriteTime); - }); + SortSaveList(); loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) { OnClicked = (btn, obj) => { - if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } - LoadGame?.Invoke(saveList.SelectedData as string); + if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } + if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } + LoadGame?.Invoke(saveInfo.FilePath); + return true; }, Enabled = false }; - } - + } + private bool SelectSaveFile(GUIComponent component, object obj) { - string fileName = (string)obj; + if (obj is not CampaignMode.SaveInfo saveInfo) { return true; } + + string fileName = saveInfo.FilePath; XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); if (doc?.Root == null) @@ -701,72 +683,55 @@ namespace Barotrauma RemoveSaveFrame(); - string subName = doc.Root.GetAttributeString("submarine", ""); - string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); - DateTime? time = null; - if (long.TryParse(saveTime, out long unixTime)) - { - time = ToolBox.Epoch.ToDateTime(unixTime); - saveTime = time.ToString(); - } + string subName = saveInfo.SubmarineName; + LocalizedString saveTime = saveInfo.SaveTime + .Select(t => (LocalizedString)t.ToLocalUserString()) + .Fallback(TextManager.Get("Unknown")); string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); - var saveFileFrame = new GUIFrame(new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) - { - RelativeOffset = new Vector2(0.0f, 0.1f) - }, style: "InnerFrame") + var saveFileFrame = new GUIFrame( + new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) + { + RelativeOffset = new Vector2(0.0f, 0.1f) + }, style: "InnerFrame") { UserData = "savefileframe" }; - var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) - { - RelativeOffset = new Vector2(0, 0.05f) - }, + var titleText = new GUITextBlock( + new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) + { + RelativeOffset = new Vector2(0, 0.05f) + }, Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); - var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) - { - RelativeOffset = new Vector2(0, 0.1f) - }); + var layoutGroup = new GUILayoutGroup( + new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) + { + RelativeOffset = new Vector2(0, 0.1f) + }); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) }, TextManager.Get("Delete"), style: "GUIButtonSmall") { - UserData = fileName, + UserData = saveInfo, OnClicked = DeleteSave }; return true; } - private bool DeleteSave(GUIButton button, object obj) - { - string saveFile = obj as string; - if (obj == null) { return false; } - - LocalizedString header = TextManager.Get("deletedialoglabel"); - LocalizedString body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); - - EventEditorScreen.AskForConfirmation(header, body, () => - { - SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); - UpdateLoadMenu(prevSaveFiles.ToList()); - return true; - }); - - return true; - } - private void RemoveSaveFrame() { GUIComponent prevFrame = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 250ad651f..6cb2671ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -352,7 +352,7 @@ namespace Barotrauma if (!availableMissions.Any()) { availableMissions.Insert(0, null); } - availableMissions.AddRange(location.AvailableMissions); + availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])); missionList.Content.ClearChildren(); @@ -571,7 +571,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) + public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -592,7 +592,7 @@ namespace Barotrauma switch (selectedTab) { case CampaignMode.InteractionType.Store: - Store.SelectStore(storeIdentifier); + Store.SelectStore(npc); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); @@ -602,6 +602,7 @@ namespace Barotrauma submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; case CampaignMode.InteractionType.Map: + GameMain.GameSession?.Map?.ResetPendingSub(); //refresh mission rewards (may have been changed by e.g. a pending submarine switch) foreach (GUITextBlock rewardText in missionRewardTexts) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 254d89608..b3e163a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -25,14 +25,11 @@ namespace Barotrauma.CharacterEditor { get { - if (cam == null) + cam ??= new Camera() { - cam = new Camera() - { - MinZoom = 0.1f, - MaxZoom = 5.0f - }; - } + MinZoom = 0.1f, + MaxZoom = 5.0f + }; return cam; } } @@ -90,26 +87,26 @@ namespace Barotrauma.CharacterEditor private float spriteSheetZoom = 1; private float spriteSheetMinZoom = 0.25f; private float spriteSheetMaxZoom = 1; - private int spriteSheetOffsetY = 20; - private int spriteSheetOffsetX = 30; + private const int spriteSheetOffsetY = 20; + private const int spriteSheetOffsetX = 30; private bool hideBodySheet; private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f); private Vector2 cameraOffset; - private List selectedJoints = new List(); - private List selectedLimbs = new List(); - private HashSet editedCharacters = new HashSet(); + private readonly List selectedJoints = new List(); + private readonly List selectedLimbs = new List(); + private readonly HashSet editedCharacters = new HashSet(); private bool isEndlessRunner; private Rectangle spriteSheetRect; - private Rectangle CalculateSpritesheetRectangle() => + private Rectangle CalculateSpritesheetRectangle() => Textures == null || Textures.None() ? Rectangle.Empty : new Rectangle( - spriteSheetOffsetX, - spriteSheetOffsetY, - (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), + spriteSheetOffsetX, + spriteSheetOffsetY, + (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), (int)(Textures.Sum(t => t.Height) * spriteSheetZoom)); private const string screenTextTag = "CharacterEditor."; @@ -146,7 +143,7 @@ namespace Barotrauma.CharacterEditor var humanSpeciesName = CharacterPrefab.HumanSpeciesName; if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllSpecies.First()); + SpawnCharacter(VisibleSpecies.First()); } else { @@ -195,7 +192,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allSpecies = null; + visibleSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -217,15 +214,12 @@ namespace Barotrauma.CharacterEditor private void Reset(IEnumerable characters = null) { - if (characters == null) - { - characters = editedCharacters; - } + characters ??= editedCharacters; characters.ForEach(c => ResetParams(c)); ResetVariables(); } - private void ResetParams(Character character) + private static void ResetParams(Character character) { character.Params.Reset(true); foreach (var animation in character.AnimController.AllAnimParams) @@ -262,7 +256,10 @@ namespace Barotrauma.CharacterEditor #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; - GameMain.LightManager.LightingEnabled = true; + if (!GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = true; + } ClearWidgets(); ClearSelection(); } @@ -280,6 +277,7 @@ namespace Barotrauma.CharacterEditor #region Main methods public override void AddToGUIUpdateList() { + if (rightArea == null || leftArea == null) { return; } rightArea.AddToGUIUpdateList(); leftArea.AddToGUIUpdateList(); @@ -718,7 +716,7 @@ namespace Barotrauma.CharacterEditor cameraOffset = Vector2.Clamp(cameraOffset, min, max); } Cam.Position = targetPos + cameraOffset; - MapEntity.mapEntityList.ForEach(e => e.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); // Update widgets jointSelectionWidgets.Values.ForEach(w => w.Update((float)deltaTime)); limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -778,7 +776,7 @@ namespace Barotrauma.CharacterEditor scaledMouseSpeed = PlayerInput.MouseSpeedPerSecond * (float)deltaTime; Cam.UpdateTransform(true); Submarine.CullEntities(Cam); - Submarine.MainSub.UpdateTransform(); + Submarine.MainSub?.UpdateTransform(); // Lightmaps if (GameMain.LightManager.LightingEnabled) @@ -1362,7 +1360,7 @@ namespace Barotrauma.CharacterEditor private class WallGroup { public readonly List walls; - + public WallGroup(List walls) { this.walls = walls; @@ -1373,7 +1371,7 @@ namespace Barotrauma.CharacterEditor var clones = new List(); walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); return new WallGroup(clones); - } + } } private void CloneWalls() @@ -1390,7 +1388,7 @@ namespace Barotrauma.CharacterEditor else if (i == 2) { clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + } } } } @@ -1403,8 +1401,8 @@ namespace Barotrauma.CharacterEditor private WallGroup SelectLastClone(bool right) { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() + var lastWall = right + ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); } @@ -1439,33 +1437,35 @@ namespace Barotrauma.CharacterEditor private Identifier currentCharacterIdentifier; private Identifier selectedJob = Identifier.Empty; - private List allSpecies; - private List AllSpecies + private List visibleSpecies; + private List VisibleSpecies { get { - if (allSpecies == null) - { -#if DEBUG - allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); -#else - allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); -#endif - allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); - } - return allSpecies; + visibleSpecies ??= CharacterPrefab.Prefabs.Where(ShowCreature).OrderBy(p => p.Identifier).Select(p => p.Identifier).ToList(); + return visibleSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private bool ShowCreature(CharacterPrefab prefab) + { + Identifier speciesName = prefab.Identifier; + if (speciesName == CharacterPrefab.HumanSpeciesName) { return true; } + if (!VanillaCharacters.Contains(prefab.ContentFile)) + { + // Always show all custom characters. + return true; + } + if (CreatureMetrics.UnlockAll) { return true; } + return CreatureMetrics.Unlocked.Contains(speciesName); + } + + private IEnumerable vanillaCharacters; + private IEnumerable VanillaCharacters { get { - if (vanillaCharacters == null) - { - vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); - } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); return vanillaCharacters; } } @@ -1474,7 +1474,7 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } @@ -1482,19 +1482,19 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllSpecies.IndexOf(character.SpeciesName); + characterIndex = VisibleSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllSpecies.Count - 1) + if (characterIndex > VisibleSpecies.Count - 1) { characterIndex = 0; } @@ -1505,7 +1505,7 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllSpecies.Count - 1; + characterIndex = VisibleSpecies.Count - 1; } } @@ -1570,10 +1570,7 @@ namespace Barotrauma.CharacterEditor { wayPoint = WayPoint.GetRandom(spawnType: SpawnType.Human, sub: Submarine.MainSub); } - if (wayPoint == null) - { - wayPoint = WayPoint.GetRandom(sub: Submarine.MainSub); - } + wayPoint ??= WayPoint.GetRandom(sub: Submarine.MainSub); spawnPosition = wayPoint.WorldPosition; } @@ -1689,7 +1686,7 @@ namespace Barotrauma.CharacterEditor XElement overrideElement = null; if (duplicate != null) { - allSpecies = null; + visibleSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1825,9 +1822,9 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllSpecies.Contains(name)) + if (!VisibleSpecies.Contains(name)) { - AllSpecies.Add(name); + VisibleSpecies.Add(name); } SpawnCharacter(name, ragdollParams); limbPairEditing = false; @@ -2680,23 +2677,33 @@ namespace Barotrauma.CharacterEditor { Stretch = true }; - // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllSpecies) + foreach (CharacterPrefab prefab in CharacterPrefab.Prefabs.OrderByDescending(p => p.Identifier)) { - characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); + Identifier speciesName = prefab.Identifier; + if (ShowCreature(prefab)) + { + characterDropDown.AddItem(speciesName.Value.CapitaliseFirstInvariant(), speciesName).SetAsFirstChild(); + } + else if (!CreatureMetrics.Encountered.Contains(speciesName)) + { + // Using a matching placeholder string here ("hidden"). + var element = characterDropDown.AddItem(TextManager.Get("hiddensubmarines"), Identifier.Empty, textColor: Color.Gray * 0.75f); + element.SetAsLastChild(); + element.Enabled = false; + } } characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { Identifier characterIdentifier = (Identifier)data; + if (characterIdentifier.IsEmpty) { return true; } try { SpawnCharacter(characterIdentifier); @@ -2797,7 +2804,7 @@ namespace Barotrauma.CharacterEditor saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; @@ -2837,7 +2844,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -2975,7 +2982,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -3214,7 +3221,7 @@ namespace Barotrauma.CharacterEditor Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); } - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -3998,7 +4005,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { @@ -4107,7 +4114,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index 4a3916717..43968f02e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -6,7 +6,7 @@ namespace Barotrauma { private GUIListBox listBox; - private ContentXElement configElement; + private readonly ContentXElement configElement; private float scrollSpeed; @@ -35,6 +35,8 @@ namespace Barotrauma set { listBox.BarScroll = value; } } + public readonly GUIButton CloseButton; + public CreditsPlayer(RectTransform rectT, string configFile) : base(null, rectT) { @@ -49,6 +51,10 @@ namespace Barotrauma configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); + + CloseButton = new GUIButton(new RectTransform(new Vector2(0.1f), RectTransform, Anchor.BottomRight, maxSize: new Point(GUI.IntScale(300), GUI.IntScale(50))) + { AbsoluteOffset = new Point(GUI.IntScale(20), GUI.IntScale(20) + (Rect.Bottom - GameMain.GraphicsHeight)) }, + TextManager.Get("close")); } private void Load() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 8f5d6ca27..5745b0152 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,7 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Lights; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.Diagnostics; @@ -28,7 +27,7 @@ namespace Barotrauma public Effect ThresholdTintEffect { get; private set; } public Effect BlueprintEffect { get; set; } - public GameScreen(GraphicsDevice graphics, ContentManager content) + public GameScreen(GraphicsDevice graphics) { cam = new Camera(); cam.Translate(new Vector2(-10.0f, 50.0f)); @@ -39,20 +38,13 @@ namespace Barotrauma CreateRenderTargets(graphics); }; - Effect LoadEffect(string path) - => content.Load(path -#if LINUX || OSX - +"_opengl" -#endif - ); - //var blurEffect = LoadEffect("Effects/blurshader"); - damageEffect = LoadEffect("Effects/damageshader"); - PostProcessEffect = LoadEffect("Effects/postprocess"); - GradientEffect = LoadEffect("Effects/gradientshader"); - GrainEffect = LoadEffect("Effects/grainshader"); - ThresholdTintEffect = LoadEffect("Effects/thresholdtint"); - BlueprintEffect = LoadEffect("Effects/blueprintshader"); + damageEffect = EffectLoader.Load("Effects/damageshader"); + PostProcessEffect = EffectLoader.Load("Effects/postprocess"); + GradientEffect = EffectLoader.Load("Effects/gradientshader"); + GrainEffect = EffectLoader.Load("Effects/grainshader"); + ThresholdTintEffect = EffectLoader.Load("Effects/thresholdtint"); + BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageEffect.Parameters["xStencil"].SetValue(damageStencil); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 4053827f4..156e58cec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,7 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private GUIDropDown serverExecutableDropdown; + private GUIDropDown languageDropdown, serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -466,6 +466,11 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => + { + SelectTab(Tab.Empty); + return true; + }; } private void CreateTutorialTab() @@ -893,12 +898,14 @@ namespace Barotrauma #endif } - string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + - " -public " + isPublicBox.Selected.ToString() + - " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + - " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -maxplayers " + maxPlayersBox.Text; + string arguments = + "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + + " -public " + isPublicBox.Selected.ToString() + + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + + " -karmaenabled " + (!karmaBox.Selected).ToString() + + " -maxplayers " + maxPlayersBox.Text + + $" -language \"{(LanguageIdentifier)languageDropdown.SelectedData}\""; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) { @@ -994,7 +1001,7 @@ namespace Barotrauma || item.IsDownloadPending || (item.InstallTime.TryGetValue(out var workshopInstallTime) && pkg.InstallTime.TryUnwrap(out var localInstallTime) - && localInstallTime < workshopInstallTime))); + && localInstallTime.ToUtcValue() < workshopInstallTime))); modUpdateStatus = (DateTime.Now + ModUpdateInterval, count); } @@ -1099,7 +1106,7 @@ namespace Barotrauma if (i == 0) { GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) + if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } @@ -1204,45 +1211,28 @@ namespace Barotrauma { menuTabs[Tab.HostServer].ClearChildren(); - string name = ""; - string password = ""; - int maxPlayers = 8; - bool isPublic = true; - bool banAfterWrongPassword = false; - bool karmaEnabled = true; - string selectedKarmaPreset = ""; - PlayStyle selectedPlayStyle = PlayStyle.Casual; - if (File.Exists(ServerSettings.SettingsFile)) + var serverSettings = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile, out _)?.Root ?? new XElement("serversettings"); + + var name = serverSettings.GetAttributeString("name", ""); + var password = serverSettings.GetAttributeString("password", ""); + var isPublic = serverSettings.GetAttributeBool("IsPublic", true); + var banAfterWrongPassword = serverSettings.GetAttributeBool("banafterwrongpassword", false); + + int maxPlayersElement = serverSettings.GetAttributeInt("maxplayers", 8); + if (maxPlayersElement > NetConfig.MaxPlayers) { - XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); - if (settingsDoc != null) - { - name = settingsDoc.Root.GetAttributeString("name", name); - password = settingsDoc.Root.GetAttributeString("password", password); - isPublic = settingsDoc.Root.GetAttributeBool("public", isPublic); - banAfterWrongPassword = settingsDoc.Root.GetAttributeBool("banafterwrongpassword", banAfterWrongPassword); - - int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); - if (maxPlayersElement > NetConfig.MaxPlayers) - { - DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); - maxPlayersElement = NetConfig.MaxPlayers; - } - - maxPlayers = maxPlayersElement; - karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); - selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); - string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); - Enum.TryParse(playStyleStr, out selectedPlayStyle); - } + DebugConsole.AddWarning($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead."); } + int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); + + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.04f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.95f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.01f, Stretch = true @@ -1320,7 +1310,7 @@ namespace Barotrauma //other settings ----------------------------------------------------- //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: name, textAlignment: textAlignment) @@ -1372,6 +1362,21 @@ namespace Barotrauma }; label.RectTransform.IsFixedSize = true; + var languageLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("Language"), textAlignment: textAlignment); + languageDropdown = new GUIDropDown(new RectTransform(textFieldSize, languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(language.Label, language.Identifier); + } + var defaultLanguage = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + var settingsLanguage = serverSettings.GetAttributeIdentifier("language", defaultLanguage.Value).ToLanguageIdentifier(); + if (!ServerLanguageOptions.Options.Any(o => o.Identifier == settingsLanguage)) + { + settingsLanguage = defaultLanguage; + } + languageDropdown.Select(ServerLanguageOptions.Options.FindIndex(o => o.Identifier == settingsLanguage)); + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerExecutable"), textAlignment: textAlignment); const string vanillaServerOption = "Vanilla"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index e92185c7e..244167045 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -189,7 +189,8 @@ namespace Barotrauma } public IReadOnlyList GetSubList() - => SubList.Content.Children.Select(c => c.UserData as SubmarineInfo).ToArray(); + => (IReadOnlyList)GameMain.Client?.ServerSubmarines + ?? Array.Empty(); public readonly GUIListBox PlayerList; @@ -300,7 +301,7 @@ namespace Barotrauma levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); - backgroundSprite = LocationType.Random(new MTRandom(intSeed))?.GetPortrait(intSeed); + backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); SeedBox.Text = levelSeed; } } @@ -929,6 +930,8 @@ namespace Barotrauma var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); + //leave some padding for the vote count text + modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; @@ -981,7 +984,7 @@ namespace Barotrauma } else { - GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); } return true; } @@ -1931,7 +1934,7 @@ namespace Barotrauma var selectedSub = component.UserData as SubmarineInfo; if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { - if (selectedSub.Price > CampaignSetupUI.CurrentSettings.InitialMoney) + if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); } @@ -3289,16 +3292,15 @@ namespace Barotrauma { //campaign running settingsBlocker.Visible = true; - CampaignFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); - ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageRound)); - QuitCampaignButton.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); + ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible; CampaignSetupFrame.Visible = false; } else { CampaignFrame.Visible = false; CampaignSetupFrame.Visible = true; - if (!GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)) + if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound)) { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), @@ -3311,7 +3313,7 @@ namespace Barotrauma foreach (var subElement in SubList.Content.Children) { var sub = subElement.UserData as SubmarineInfo; - bool tooExpensive = sub.Price > CampaignSetupUI.CurrentSettings.InitialMoney; + bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney; if (tooExpensive || !sub.IsCampaignCompatible) { foreach (var textBlock in subElement.GetAllChildren()) @@ -3362,7 +3364,7 @@ namespace Barotrauma CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } RefreshEnabledElements(); - if (enabled) + if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 64a7204c0..5d1831408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Xml.Linq; namespace Barotrauma @@ -241,6 +239,7 @@ namespace Barotrauma private GUITickBox filterPassword; private GUITickBox filterFull; private GUITickBox filterEmpty; + private GUIDropDown languageDropdown; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; @@ -255,6 +254,7 @@ namespace Barotrauma private TernaryOption filterModdedValue = TernaryOption.Any; private ColumnLabel sortedBy; + private bool sortedAscending = true; private const float sidebarWidth = 0.2f; public ServerListScreen() @@ -425,10 +425,13 @@ namespace Barotrauma ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); + RectTransform createFilterRectT() + => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform); + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + var tickBox = new GUITickBox(createFilterRectT(), text) { UserData = text, Selected = defaultState, @@ -450,6 +453,109 @@ namespace Barotrauma filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + // Language filter + if (ServerLanguageOptions.Options.Any()) + { + var languageKey = "Language".ToIdentifier(); + var allLanguagesKey = "AllLanguages".ToIdentifier(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true); + + languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey); + var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild(); + + // Spacer between "All" and the individual languages + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform) + { + MinSize = new Point(0, GUI.IntScaleCeiling(2)) + }, style: null) + { + Color = Color.DarkGray, + CanBeFocused = false + }; + + var selectedLanguages + = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray( + languageKey, + Array.Empty()); + foreach (var (label, identifier, _) in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(label, identifier); + } + + if (!selectedLanguages.Any()) + { + selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray(); + } + + foreach (var lang in selectedLanguages) + { + languageDropdown.SelectItem(lang); + } + + if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l))) + { + languageDropdown.SelectItem(allLanguagesKey); + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + + var langTickboxes = languageDropdown.ListBox.Content.Children + .Where(c => c.UserData is LanguageIdentifier) + .Select(c => c.GetChild()) + .ToArray(); + + bool inSelectedCall = false; + languageDropdown.OnSelected = (_, userData) => + { + if (inSelectedCall) { return true; } + try + { + inSelectedCall = true; + + if (Equals(allLanguagesKey, userData)) + { + foreach (var tb in langTickboxes) + { + tb.Selected = allTickbox.Selected; + } + } + + bool noneSelected = langTickboxes.All(tb => !tb.Selected); + bool allSelected = langTickboxes.All(tb => tb.Selected); + + if (allSelected != allTickbox.Selected) + { + allTickbox.Selected = allSelected; + } + + if (allSelected) + { + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + else if (noneSelected) + { + languageDropdown.Text = TextManager.Get("None"); + } + + var languages = languageDropdown.SelectedDataMultiple.OfType(); + + ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages)); + GameSettings.SaveCurrentConfig(); + return true; + } + finally + { + inSelectedCall = false; + FilterServers(); + } + }; + } + // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { @@ -713,7 +819,7 @@ namespace Barotrauma private void SortList(ColumnLabel sortBy, bool toggle) { - if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; } sortedBy = sortBy; @@ -730,51 +836,74 @@ namespace Barotrauma } } - bool ascending = arrowUp.Visible; + sortedAscending = arrowUp.Visible; if (toggle) { - ascending = !ascending; + sortedAscending = !sortedAscending; } - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; + arrowUp.Visible = sortedAscending; + arrowDown.Visible = !sortedAscending; serverList.Content.RectTransform.SortChildren((c1, c2) => { - if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } - if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } - - switch (sortBy) - { - case ColumnLabel.ServerListCompatible: - bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); - bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); - - if (s1Compatible == s2Compatible) { return 0; } - return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListHasPassword: - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListName: - // I think we actually want culture-specific sorting here? - return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); - case ColumnLabel.ServerListRoundStarted: - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPlayers: - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPing: - return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch - { - (false, false) => 0, - (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), - (false, true) => 1, - (true, false) => -1 - }; - default: - return 0; - } + if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } + if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } + int comparison = sortedAscending ? 1 : -1; + return CompareServer(sortBy, s1, s2) * comparison; }); } + + private void InsertServer(ServerInfo serverInfo, GUIComponent component) + { + var children = serverList.Content.RectTransform.Children.Reverse().ToList(); + + int comparison = sortedAscending ? 1 : -1; + foreach (var child in children) + { + if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } + if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0) + { + var index = serverList.Content.RectTransform.GetChildIndex(child); + component.RectTransform.RepositionChildInHierarchy(index + 1); + return; + } + } + component.RectTransform.SetAsFirstChild(); + } + + private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2) + { + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return s1Compatible ? -1 : 1; + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return s1.HasPassword ? 1 : -1; + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return s1.GameStarted ? 1 : -1; + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + } public override void Select() { @@ -821,6 +950,7 @@ namespace Barotrauma UpdateFriendsList(); panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -840,7 +970,7 @@ namespace Barotrauma RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } + if (child.UserData is not ServerInfo serverInfo) { continue; } child.Visible = ShouldShowServer(serverInfo); } @@ -851,6 +981,20 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } + private bool AllLanguagesVisible + { + get + { + if (languageDropdown is null) { return true; } + + // CountChildren-1 because there's a separator element in there that can't be selected + int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1; + int selectedCount = languageDropdown.SelectedIndexMultiple.Count(); + + return selectedCount >= tickBoxCount; + } + } + private bool ShouldShowServer(ServerInfo serverInfo) { #if !DEBUG @@ -918,6 +1062,14 @@ namespace Barotrauma } } + if (!AllLanguagesVisible) + { + if (!languageDropdown.SelectedDataMultiple.OfType().Contains(serverInfo.Language)) + { + return false; + } + } + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (Identifier)tickBox.UserData; @@ -1031,8 +1183,8 @@ namespace Barotrauma if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } if (info.IsInServer - && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } - && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + && info.ConnectCommand.TryUnwrap(out var command) + && command.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { const int framePadding = 5; @@ -1270,7 +1422,7 @@ namespace Barotrauma serverPreview.Content.ClearChildren(); panelAnimator.RightEnabled = false; joinButton.Enabled = false; - selectedServer = null; + selectedServer = Option.None; if (selectedTab == TabEnum.All) { @@ -1370,8 +1522,7 @@ namespace Barotrauma UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } - SortList(sortedBy, toggle: false); - FilterServers(); + InsertServer(serverInfo, serverFrame); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1629,7 +1780,7 @@ namespace Barotrauma #endif } - private Color GetPingTextColor(int ping) + private static Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); @@ -1664,6 +1815,7 @@ namespace Barotrauma { ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } + GameSettings.SaveCurrentConfig(); } public void LoadServerFilters() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 4b0ee066b..064f9819e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1158,7 +1158,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1177,7 +1177,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1306,7 +1306,6 @@ namespace Barotrauma try { assemblyPrefab.Delete(); - UpdateEntityList(); OpenEntityMenu(MapEntityCategory.ItemAssembly); } catch (Exception e) @@ -2725,11 +2724,13 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true); GUIButton createTabberBtn(string labelTag) { var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.TextBlock.Wrap = true; + btn.TextBlock.SetTextPos(); btn.RectTransform.MaxSize = RectTransform.MaxPoint; btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); btn.Font = GUIStyle.SmallFont; @@ -3088,9 +3089,17 @@ namespace Barotrauma string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } - + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); - doc.SaveSafe(filePath); + try + { + doc.SaveSafe(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to save the item assembly to \"{filePath}\".", e); + return; + } var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); if (!result.TryUnwrapSuccess(out var resultPackage)) @@ -3644,6 +3653,8 @@ namespace Barotrauma private void OpenEntityMenu(MapEntityCategory? entityCategory) { + UpdateEntityList(); + foreach (GUIButton categoryButton in entityCategoryButtons) { categoryButton.Selected = entityCategory.HasValue ? @@ -3771,14 +3782,14 @@ namespace Barotrauma { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? - MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : + List targets = MapEntity.HighlightedEntities.Any(me => !MapEntity.SelectedList.Contains(me)) ? + MapEntity.HighlightedEntities.ToList() : new List(MapEntity.SelectedList); Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn var container = item.GetComponent(); @@ -4023,7 +4034,7 @@ namespace Barotrauma pickerMutex = new object(), hexMutex = new object(); - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); + Vector2 relativeSize = new Vector2(0.4f * GUI.AspectRatioAdjustment, 0.3f); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { @@ -4062,24 +4073,31 @@ namespace Barotrauma GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight); float currentHue = colorPicker.SelectedHue / 360f; - GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; 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: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + 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); + GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; 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: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + 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); + GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; 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: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + 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 }; + GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.1f + }; - new GUICustomComponent(new RectTransform(new Vector2(0.4f, 0.8f), colorInfoLayout.RectTransform), (batch, component) => + new GUICustomComponent(new RectTransform(Vector2.One, colorInfoLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), (batch, component) => { Rectangle rect = component.Rect; Point areaSize = new Point(rect.Width, rect.Height / 2); @@ -5251,9 +5269,9 @@ namespace Barotrauma { if (dummyCharacter.SelectedItem == null) { - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.HighlightedEntities) { - if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { var container = item.GetComponents().ToList(); if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index fb7cc1008..7cf2b716b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -1,6 +1,5 @@ #nullable enable using System.Linq; -using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -34,7 +33,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index c3c4c3873..4df190a1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -9,7 +9,7 @@ using System.Diagnostics; namespace Barotrauma { - class SerializableEntityEditor : GUIComponent + sealed class SerializableEntityEditor : GUIComponent { private readonly int elementHeight; private readonly GUILayoutGroup layoutGroup; @@ -399,10 +399,6 @@ namespace Barotrauma { propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); } - else if (value is string stringVal) - { - propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); - } else if (value.GetType().IsEnum) { if (value.GetType().IsDefined(typeof(FlagsAttribute), inherit: false)) @@ -450,6 +446,10 @@ namespace Barotrauma { propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip); } + else if (value is string or Identifier) + { + propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip); + } return propertyField; } @@ -696,7 +696,7 @@ namespace Barotrauma propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => { - if (!propertyBox.Selected) { propertyBox.Text = (string)property.GetValue(entity); } + if (!propertyBox.Selected) { propertyBox.Text = property.GetValue(entity).ToString(); } }; bool OnApply(GUITextBox textBox) @@ -714,7 +714,7 @@ namespace Barotrauma if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = (string) property.GetValue(entity); + textBox.Text = property.GetValue(entity).ToString(); textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs index 48bde97dc..d96f57b72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -1,20 +1,18 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - #warning TODO: implement properly public class ServerListFilters { private readonly Dictionary attributes = new Dictionary(); - private ServerListFilters() { } - - private ServerListFilters(XElement elem) + private ServerListFilters(XElement? elem) { - if (elem == null) { return; } + if (elem is null) { return; } foreach (var attr in elem.Attributes()) { attributes.Add(attr.NameAsIdentifier(), attr.Value); @@ -23,8 +21,6 @@ namespace Barotrauma public static void Init(XElement? elem) { - if (elem is null) { return; } - Instance = new ServerListFilters(elem); } @@ -50,17 +46,27 @@ namespace Barotrauma { if (attributes.TryGetValue(key, out string? val)) { - if (Enum.TryParse(val, out T result)) { return result; } + if (Enum.TryParse(val, ignoreCase: true, out T result)) { return result; } } return def; } + public LanguageIdentifier[] GetAttributeLanguageIdentifierArray(Identifier key, LanguageIdentifier[] def) + { + return attributes.TryGetValue(key, out string? val) + ? val.Split(",") + .Select(static s => s.Trim()) + .Where(static s => !s.IsNullOrWhiteSpace()) + .Select(static s => s.ToLanguageIdentifier()).ToArray() + : def; + } + public void SetAttribute(Identifier key, string val) { attributes[key] = val; } - public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(null); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 835981330..575e822eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -648,6 +648,12 @@ namespace Barotrauma.Sounds if (isConnected == 0) { + if (!GameMain.Instance.HasLoaded) + { + //wait for loading to finish so we don't start releasing and reloading sounds when they're being loaded, + //or throw an error mid-loading that'd prevent the content package from being enabled + return; + } DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); SetAudioOutputDevice(""); Disconnected = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 7b89b66bc..6287642f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -42,12 +42,7 @@ namespace Barotrauma { if (effect == null) { -#if WINDOWS - effect = GameMain.Instance.Content.Load("Effects/deformshader"); -#endif -#if LINUX || OSX - effect = GameMain.Instance.Content.Load("Effects/deformshader_opengl"); -#endif + effect = EffectLoader.Load("Effects/deformshader"); } Invert = invert; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 258594f3d..1c1cda216 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -117,6 +117,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + currentLobby?.SetData("language", serverSettings.Language.ToString()); DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index b8688c2f8..1089221b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -202,6 +202,7 @@ namespace Barotrauma.Steam ContentPackageManager.EnabledPackages.Core!, (p) => { }, heightScale: 1.0f / 13.0f); + enabledCoreDropdown.AllowNonText = true; Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); topRight.ChildAnchor = Anchor.CenterLeft; @@ -535,34 +536,119 @@ namespace Barotrauma.Steam bulkUpdateButton.Enabled = false; bulkUpdateButton.ToolTip = ""; ContentPackageManager.UpdateContentPackageList(); - + + var corePackages = ContentPackageManager.CorePackages.ToArray(); + var currentCore = ContentPackageManager.EnabledPackages.Core!; SwapDropdownValues(enabledCoreDropdown, (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, + corePackages, + currentCore, (p) => { + // Manually set dropdown text because + // adding buttons to the elements breaks + // this part of the dropdown code + enabledCoreDropdown.Text = p.Name; enabledCoreDropdown.ButtonTextColor = p.HasAnyErrors ? GUIStyle.Red : GUIStyle.TextColorNormal; }); - enabledCoreDropdown.ListBox.Content.Children - .OfType() - .ForEach(tb => - CreateModErrorInfo( - (tb.UserData as ContentPackage)!, - tb, - tb)); - - void addRegularModToList(RegularPackage mod, GUIListBox list) + + void addButtonForMod(ContentPackage mod, GUILayoutGroup parent) { - var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, parent.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, parent.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.UgcId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (isUpToDate) { return; } + + infoButton.CanBeSelected = true; + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + }); + } + } + + GUILayoutGroup createBaseModListUi(ContentPackage mod, GUIListBox listBox, float height) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, height), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = mod }; + + 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 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 + }; + CreateModErrorInfo(mod, modFrame, modName); + addButtonForMod(mod, frameContent); + return frameContent; + } + + foreach (var element in enabledCoreDropdown.ListBox.Content.Children.ToArray()) + { + enabledCoreDropdown.ListBox.RemoveChild(element); + if (element.UserData is not ContentPackage mod) { continue; } + + createBaseModListUi(mod, enabledCoreDropdown.ListBox, 0.24f); + } + enabledCoreDropdown.Select(corePackages.IndexOf(currentCore)); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var frameContent = createBaseModListUi(mod, list, 0.08f); + + var modFrame = frameContent.Parent; + var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), onUpdate: (f, component) => { @@ -639,76 +725,13 @@ namespace Barotrauma.Steam 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 - }; - 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.UgcId}", - 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"); - } - }); - } + dragIndicator.RectTransform.SetAsFirstChild(); } void addRegularModsToList(IEnumerable mods, GUIListBox list) @@ -729,7 +752,7 @@ namespace Barotrauma.Steam .Where(p => ContentPackageManager.RegularPackages.Contains(p))) .ToArray(); var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); - + addRegularModsToList(enabledMods, enabledRegularModsList); if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } @@ -747,7 +770,7 @@ namespace Barotrauma.Steam var mod = child.UserData as RegularPackage; if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; } - if (!(ugcId is SteamWorkshopId workshopId)) { continue; } + if (ugcId is not SteamWorkshopId workshopId) { continue; } var btn = child.GetChild()?.GetAllChildren().Last(); if (btn is null) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index 7a3a9b85d..12b4b2876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -113,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - if (pkg.UgcId.TryUnwrap(out ContentPackageId ugcId)) + if (pkg.UgcId.TryUnwrap(out var ugcId)) { pkgElem.SetAttributeValue("id", ugcId.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index d99e8887b..fac6cd8cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -68,10 +68,13 @@ namespace Barotrauma.Steam } protected static void SwapDropdownValues( - GUIDropDown dropdown, Func textFunc, IReadOnlyList values, T currentValue, + GUIDropDown dropdown, + Func textFunc, + IReadOnlyList values, + T currentValue, Action setter) { - if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T))) + if (dropdown.ListBox.Content.Children.Any(c => c.UserData is not T)) { throw new Exception("SwapValues must preserve the type of the dropdown's userdata"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs new file mode 100644 index 000000000..5a34c929f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework.Graphics; +namespace Barotrauma; + +static class EffectLoader +{ + public static Effect Load(string path) + => GameMain.Instance.Content.Load(path +#if LINUX || OSX + +"_opengl" +#endif + ); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index ab4b38403..45111625f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -368,6 +368,7 @@ namespace Barotrauma xmlContent.Add($""); + conversationClosingIndent.Clear(); int conversationStart = 1; xmlContent.Add(string.Empty); @@ -419,9 +420,9 @@ namespace Barotrauma { string[] nextConversationElement = csvContent[i + 1].Split(separator); - if (nextConversationElement[1] != string.Empty) + if (nextConversationElement[3] != string.Empty) { - nextDepth = int.Parse(nextConversationElement[2]); + nextDepth = int.Parse(nextConversationElement[3]); nextIsSubConvo = nextDepth > depthIndex; } @@ -441,7 +442,12 @@ namespace Barotrauma } else { + //end of file, close remaining xml tags xmlContent.Add(element.TrimEnd() + "/>"); + for (int j = depthIndex - 1; j >= 0; j--) + { + HandleClosingElements(xmlContent, j); + } } } @@ -453,12 +459,12 @@ namespace Barotrauma private static void HandleClosingElements(List xmlContent, int targetDepth) { - if (conversationClosingIndent.Count == 0) return; + if (conversationClosingIndent.Count == 0) { return; } for (int k = conversationClosingIndent.Count - 1; k >= 0; k--) { int currentIndent = conversationClosingIndent[k]; - if (currentIndent < targetDepth) break; + if (currentIndent < targetDepth) { break; } xmlContent.Add($"{GetIndenting(currentIndent)}"); conversationClosingIndent.RemoveAt(k); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index 43ba00158..f7c0b35fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -7,28 +7,30 @@ using System.Text; namespace Barotrauma { - class SpriteRecorder : ISpriteBatch, IDisposable + sealed class SpriteRecorder : ISpriteBatch, IDisposable { - private struct Command + public readonly record struct Command( + Texture2D Texture, + VertexPositionColorTexture VertexBL, + VertexPositionColorTexture VertexBR, + VertexPositionColorTexture VertexTL, + VertexPositionColorTexture VertexTR, + float Depth, + Vector2 Min, + Vector2 Max, + int Index) { - public readonly Texture2D Texture; - public readonly VertexPositionColorTexture VertexBL; - public readonly VertexPositionColorTexture VertexBR; - public readonly VertexPositionColorTexture VertexTL; - public readonly VertexPositionColorTexture VertexTR; - public readonly float Depth; - public readonly Vector2 Min; - public readonly Vector2 Max; - public readonly int Index; - - public bool Overlaps(Command other) - { - return - Min.X <= other.Max.X && Max.X >= other.Min.X && - Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; - } - - public Command( + public static Vector2 GetMinPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Min(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Min(vertices.Select(v => v.Position.Y).ToArray())); + + public static Vector2 GetMaxPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Max(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Max(vertices.Select(v => v.Position.Y).ToArray())); + + public static Command FromTransform( Texture2D texture, Vector2 pos, Rectangle srcRect, @@ -46,15 +48,11 @@ namespace Barotrauma int srcRectBottom = srcRect.Bottom; if (effects.HasFlag(SpriteEffects.FlipHorizontally)) { - var temp = srcRectRight; - srcRectRight = srcRectLeft; - srcRectLeft = temp; + (srcRectRight, srcRectLeft) = (srcRectLeft, srcRectRight); } if (effects.HasFlag(SpriteEffects.FlipVertically)) { - var temp = srcRectBottom; - srcRectBottom = srcRectTop; - srcRectTop = temp; + (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } rotation = MathHelper.ToRadians(rotation); @@ -68,59 +66,63 @@ namespace Barotrauma pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; - Texture = texture; + var vertexTl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X, pos.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - Depth = depth; + var vertexTr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - VertexTL.Color = color; - VertexTR.Color = color; - VertexBL.Color = color; - VertexBR.Color = color; + var vertexBl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); - VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); - VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); - VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); + var vertexBr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - Min = new Vector2( - MathUtils.Min - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Min - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var min = GetMinPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - Max = new Vector2( - MathUtils.Max - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Max - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var max = GetMaxPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - - Index = index; + return new Command( + texture, + vertexBl, + vertexBr, + vertexTl, + vertexTr, + depth, + min, + max, + index); + } + public bool Overlaps(Command other) + { + return + Min.X <= other.Max.X && Max.X >= other.Min.X && + Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; } } @@ -151,8 +153,8 @@ namespace Barotrauma public static BasicEffect BasicEffect = null; - private List recordedBuffers = new List(); - private List commandList = new List(); + private readonly List recordedBuffers = new List(); + private readonly List commandList = new List(); private SpriteSortMode currentSortMode; private IndexBuffer indexBuffer = null; @@ -170,16 +172,45 @@ namespace Barotrauma currentSortMode = sortMode; } - public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + private void AppendCommand(Command command) { if (isDisposed) { return; } - - Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0); + if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); - commandList?.Add(command); + commandList.Add(command); + } + + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + { + if (isDisposed) { return; } + + var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count); + AppendCommand(command); + } + + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) + { + if (isDisposed) { return; } + + int iters = count ?? (vertices.Length / 4); + for (int i=0;iBarotrauma FakeFish, Undertow Games Barotrauma - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index f458b5f64..6634acbb0 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb index 7d48e26be..57132f551 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb @@ -79,3 +79,8 @@ /processorParam:DebugMode=Auto /build:blueprintshader.fx +#begin wearableclip.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:wearableclip.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index 82d54dedf..c5b56f9eb 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -78,3 +78,9 @@ /processor:EffectProcessor /processorParam:DebugMode=Auto /build:thresholdtint_opengl.fx + +#begin wearableclip_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:wearableclip_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx new file mode 100644 index 000000000..4ea589cf4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -0,0 +1,42 @@ + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float stencilSample(float2 texCoord, float2 offset) +{ + return xStencil.Sample( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx new file mode 100644 index 000000000..25dd7f3d3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -0,0 +1,42 @@ + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float stencilSample(float2 texCoord, float2 offset) +{ + return tex2D( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * color; + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index eaf4449e0..4e65c1f58 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5f96cbfd2..17eeb1278 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index b19369ff4..a458e043f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 61369bc00..ae0c01dfb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -14,6 +14,13 @@ namespace Barotrauma /// public bool Discarded; + public void ApplyDeathEffects() + { + RespawnManager.ReduceCharacterSkills(this); + RemoveSavedStatValuesOnDeath(); + CauseOfDeath = null; + } + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7588aaf7b..5eff578d3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -8,8 +8,9 @@ namespace Barotrauma { partial class Character { - public Address OwnerClientAddress; - public string OwnerClientName; + private Address ownerClientAddress; + private Option ownerClientAccountId; + public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -19,6 +20,35 @@ namespace Barotrauma public bool HealthUpdatePending; + public void SetOwnerClient(Client client) + { + if (client == null) + { + ownerClientAddress = null; + ownerClientAccountId = Option.None(); + IsRemotePlayer = false; + } + else + { + ownerClientAddress = client.Connection.Endpoint.Address; + ownerClientAccountId = client.AccountId; + IsRemotePlayer = true; + } + } + + public bool IsClientOwner(Client client) + { + if (ownerClientAccountId.TryUnwrap(out var accountId) + && client.AccountId.TryUnwrap(out var clientId)) + { + return accountId == clientId; + } + else + { + return ownerClientAddress == client.Connection.Endpoint.Address; + } + } + public float GetPositionUpdateInterval(Client recipient) { if (!Enabled) { return 1000.0f; } @@ -662,6 +692,7 @@ namespace Barotrauma { msg.WriteIdentifier(MerchantIdentifier); } + msg.WriteIdentifier(Faction); int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs new file mode 100644 index 000000000..5842632f7 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal static class HealingCooldown + { + private static readonly Dictionary HealingCooldowns = new(); + + // Little bit less than client's 0.5 second cooldown to account for latency + private const float CooldownDuration = 0.4f; + + public static bool IsOnCooldown(Client client) + { + RemoveExpiredCooldowns(); + return HealingCooldowns.ContainsKey(client); + } + + public static void SetCooldown(Client client) + { + RemoveExpiredCooldowns(); + DateTimeOffset newCooldown = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); + HealingCooldowns[client] = newCooldown; + } + + private static void RemoveExpiredCooldowns() + { + HashSet? expiredCooldowns = null; + + DateTimeOffset now = DateTimeOffset.UtcNow; + + foreach (var (client, cooldown) in HealingCooldowns) + { + if (now < cooldown) { continue; } + + expiredCooldowns ??= new HashSet(); + expiredCooldowns.Add(client); + } + + if (expiredCooldowns is null) { return; } + + foreach (Client expiredCooldown in expiredCooldowns) + { + HealingCooldowns.Remove(expiredCooldown); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 086c12192..25c94259e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1374,7 +1374,7 @@ namespace Barotrauma MultiPlayerCampaign.StartCampaignSetup(); return; } - if (!GameMain.Server.StartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } + if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } } })); @@ -1401,6 +1401,44 @@ namespace Barotrauma })); + commands.Add(new Command("forcelocationtypechange", "", (string[] args) => + { + if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } + + if (args.Length < 2) + { + ThrowError("Invalid parameters. The command should be formatted as \"forcelocationtypechange [locationname] [locationtype]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (location == null) + { + ThrowError($"Could not find a location with the name {args[0]}."); + return; + } + + var locationType = LocationType.Prefabs.FirstOrDefault(lt => + lt.Name.Equals(args[1], StringComparison.OrdinalIgnoreCase) || lt.Identifier == args[1]); + if (location == null) + { + ThrowError($"Could not find the location type {args[1]}."); + return; + } + + location.ChangeType(GameMain.GameSession.Campaign, locationType); + }, + () => + { + if (GameMain.GameSession?.Campaign == null) { return null; } + + return new string[][] + { + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() + }; + })); + AssignOnExecute("resetcharacternetstate", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1928,7 +1966,7 @@ namespace Barotrauma { GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); } - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); } ); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs new file mode 100644 index 000000000..5369ec7e1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -0,0 +1,43 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class MissionAction : EventAction + { + private static readonly HashSet missionsUnlockedThisRound = new HashSet(); + + public static void ResetMissionsUnlockedThisRound() + { + missionsUnlockedThisRound.Clear(); + } + + public static void NotifyMissionsUnlockedThisRound(Client client) + { + foreach (Mission mission in missionsUnlockedThisRound) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission, Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); + outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[0]) ?? -1); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[1]) ?? -1); + outmsg.WriteString(mission.Name.Value); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index ea3362d68..f41f62988 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -84,9 +84,6 @@ namespace Barotrauma Console.WriteLine("Loading game settings"); GameSettings.Init(); - Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.Cache.Load(); - Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); @@ -182,7 +179,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-name": name = CommandLineArgs[i + 1]; @@ -248,7 +245,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-playstyle": Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle); @@ -270,6 +267,14 @@ namespace Barotrauma Server.ServerSettings.KarmaPreset = karmaPresetName; i++; break; + case "-language": + LanguageIdentifier language = CommandLineArgs[i + 1].ToLanguageIdentifier(); + if (ServerLanguageOptions.Options.Any(o => o.Identifier == language)) + { + Server.ServerSettings.Language = language; + } + i++; + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 4283c7206..f3d53216a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -27,12 +27,9 @@ namespace Barotrauma AnyOneAllowedToManageCampaign(permissions); } - public bool AllowedToManageWallets(Client client) + public static bool AllowedToManageWallets(Client client) { - return - client.HasPermission(ClientPermissions.ManageCampaign) || - client.HasPermission(ClientPermissions.ManageMoney) || - IsOwner(client); + return AllowedToManageCampaign(client, ClientPermissions.ManageMoney); } public override void ShowStartMessage() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 138dc3cfe..d99340d6b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -109,15 +109,22 @@ namespace Barotrauma return AccountId == other.AccountId && other.ClientAddress == ClientAddress; } + public void Reset() + { + itemData = null; + healthData = null; + WalletData = null; + } + public void SpawnInventoryItems(Character character, Inventory inventory) { if (character == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items. Character was null."); + throw new InvalidOperationException($"Failed to spawn inventory items. Character was null."); } if (itemData == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); + throw new InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); } character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index e71800251..b037e9888 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -85,6 +85,7 @@ namespace Barotrauma { if (purchasedHullRepairs == value) { return; } purchasedHullRepairs = value; + PurchasedHullRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -95,6 +96,7 @@ namespace Barotrauma { if (purchasedLostShuttles == value) { return; } purchasedLostShuttles = value; + PurchasedLostShuttlesInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -105,6 +107,7 @@ namespace Barotrauma { if (purchasedItemRepairs == value) { return; } purchasedItemRepairs = value; + PurchasedItemRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -146,7 +149,7 @@ namespace Barotrauma { NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData; MirrorLevel = false; - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); } public static void StartCampaignSetup() @@ -240,9 +243,7 @@ namespace Barotrauma //reduce skills if the character has died if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { - RespawnManager.ReduceCharacterSkills(characterInfo); - characterInfo.RemoveSavedStatValuesOnDeath(); - characterInfo.CauseOfDeath = null; + characterInfo.ApplyDeathEffects(); } c.CharacterInfo = characterInfo; SetClientCharacterData(c); @@ -254,13 +255,21 @@ namespace Barotrauma { if (data.HasSpawned && !GameMain.Server.ConnectedClients.Any(c => data.MatchesClient(c))) { - var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); - if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) + var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); + if (character != null && + (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) { + //character still alive (or killed by Disconnect) -> save it as-is characterData.RemoveAll(cd => cd.IsDuplicate(data)); data.Refresh(character); characterData.Add(data); } + else + { + //character dead or removed -> reduce skills, remove items, health data, etc + data.CharacterInfo.ApplyDeathEffects(); + data.Reset(); + } } } @@ -331,6 +340,7 @@ namespace Barotrauma IsFirstRound = true; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; } @@ -391,7 +401,7 @@ namespace Barotrauma //don't start the next round automatically if we just finished the campaign if (transitionType != TransitionType.End) { - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); } yield return CoroutineStatus.Success; @@ -819,54 +829,37 @@ namespace Barotrauma Bank.ForceUpdate(); } - if (purchasedHullRepairs != PurchasedHullRepairs) + if (purchasedHullRepairs && !PurchasedHullRepairs) { - switch (purchasedHullRepairs) + if (GetBalance(sender) >= hullRepairCost) { - case true when GetBalance(sender) >= hullRepairCost: - TryPurchase(sender, hullRepairCost); - PurchasedHullRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - break; - case false: - PurchasedHullRepairs = false; - personalWallet.Refund(hullRepairCost); - break; + TryPurchase(sender, hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); } } - if (purchasedItemRepairs != PurchasedItemRepairs) + if (purchasedItemRepairs && !PurchasedItemRepairs) { - switch (purchasedItemRepairs) + if (GetBalance(sender) >= itemRepairCost) { - case true when GetBalance(sender) >= itemRepairCost: - TryPurchase(sender, itemRepairCost); - PurchasedItemRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - break; - case false: - PurchasedItemRepairs = false; - personalWallet.Refund(itemRepairCost); - break; + TryPurchase(sender, itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); } } - if (purchasedLostShuttles != PurchasedLostShuttles) + if (purchasedLostShuttles && !PurchasedLostShuttles) { if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost)) + else if (TryPurchase(sender, shuttleRetrieveCost)) { PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } - else if (!purchasedItemRepairs) - { - PurchasedLostShuttles = false; - personalWallet.Refund(shuttleRetrieveCost); - } } if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) @@ -1054,49 +1047,47 @@ namespace Barotrauma if (GameMain.Server is null) { return; } - switch (transfer.Sender) + if (transfer.Sender.TryUnwrap(out var id)) { - case Some { Value: var id }: - if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } + if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - TransferMoney(wallet); - break; - case None _: - if (!AllowedToManageWallets(sender)) + TransferMoney(wallet); + } + else + { + if (!AllowedToManageWallets(sender)) + { + if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID) { - if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) - { - 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; + 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; + } - TransferMoney(Bank); - break; + TransferMoney(Bank); } void TransferMoney(Wallet from) { if (!from.TryDeduct(transfer.Amount)) { return; } - switch (transfer.Receiver) + if (transfer.Receiver.TryUnwrap(out var id)) { - case Some { Value: var id }: - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - wallet.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; - case None _: - Bank.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; + wallet.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); + } + else + { + Bank.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e46134ca6..d6695caf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -24,7 +24,11 @@ namespace Barotrauma public DateTimeOffset Expiry; } - private readonly Dictionary rateLimits = new Dictionary(); + private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); + + private readonly List afflictionSubscribers = new(); + + private readonly Dictionary rateLimits = new(); public void ServerRead(IReadMessage inc, Client sender) { @@ -35,6 +39,9 @@ namespace Barotrauma case NetworkHeader.ADD_EVERYTHING_TO_PENDING: ProcessAddEverything(sender); break; + case NetworkHeader.UNSUBSCRIBE_ME: + RemoveClientSubscription(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -72,6 +79,17 @@ namespace Barotrauma ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } + private void RemoveClientSubscription(Client client) + { + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + } + } + } + private void ProcessNewRemoval(IReadMessage inc, Client client) { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } @@ -129,6 +147,14 @@ namespace Barotrauma Afflictions = pendingAfflictions }; + if (foundInfo is not null) + { + RemoveClientSubscription(client); + + // the client subscribes to the afflictions of the crew member for the next minute + afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1))); + } + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index de8517914..188721aa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -8,10 +8,12 @@ namespace Barotrauma.Items.Components private readonly struct EventData : IEventData { public readonly bool Launch; + public readonly byte SpreadCounter; - public EventData(bool launch) + public EventData(bool launch, byte spreadCounter = 0) { Launch = launch; + SpreadCounter = spreadCounter; } } @@ -32,6 +34,7 @@ namespace Barotrauma.Items.Components msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); + msg.WriteByte(eventData.SpreadCounter); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 14901a971..65b176cff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -19,7 +20,7 @@ namespace Barotrauma bool accessible = c.Character.CanAccessInventory(this); if (this is CharacterInventory characterInventory && accessible) { - if (Owner == null || !(Owner is Character ownerCharacter)) + if (Owner == null || Owner is not Character ownerCharacter) { accessible = false; } @@ -39,7 +40,7 @@ namespace Barotrauma { foreach (ushort id in newItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item)) { continue; } + if (Entity.FindEntityByID(id) is not Item item) { continue; } item.PositionUpdateInterval = 0.0f; if (item.ParentInventory != null && item.ParentInventory != this) { @@ -94,7 +95,15 @@ namespace Barotrauma { foreach (ushort id in newItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } + if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } + + if (item.GetComponent() is not Pickable pickable || + (pickable.IsAttached && !pickable.PickingDone) || + item.AllowedSlots.None()) + { + DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})"); + continue; + } if (GameMain.Server != null) { @@ -105,7 +114,7 @@ namespace Barotrauma (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) { #if DEBUG || UNSTABLE - DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {(item.ParentInventory?.Owner.ToString() ?? "null")}). No access.", Color.Yellow); + DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); #endif if (item.body != null && !c.PendingPositionUpdates.Contains(item)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index e4cbbefcd..40c1ee0a2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -153,25 +153,27 @@ namespace Barotrauma (components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c); break; case EventType.Treatment: - if (c.Character == null || !c.Character.CanInteractWith(this)) return; + if (c.Character == null || !c.Character.CanInteractWith(this)) { return; } UInt16 characterID = msg.ReadUInt16(); byte limbIndex = msg.ReadByte(); - Character targetCharacter = FindEntityByID(characterID) as Character; - if (targetCharacter == null) break; - if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) break; + if (HealingCooldown.IsOnCooldown(c)) { return; } + if (FindEntityByID(characterID) is not Character targetCharacter) { break; } + if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) { break; } + + HealingCooldown.SetCooldown(c); Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null; - if (ContainedItems == null || ContainedItems.All(i => i == null)) + if (ContainedItems == null || ContainedItems.All(static i => i == null)) { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " used item " + Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} used item {Name}", ServerLog.MessageType.ItemInteraction); } else { GameServer.Log( - GameServer.CharacterLogName(c.Character) + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", + $"{GameServer.CharacterLogName(c.Character)} used item {Name} (contained items: {string.Join(", ", ContainedItems.Select(i => i.Name))})", ServerLog.MessageType.ItemInteraction); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 9febcddaf..8742d17eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -11,10 +11,10 @@ namespace Barotrauma.Networking { private static UInt32 LastIdentifier = 0; - public bool Expired => ExpirationTime is { } expirationTime && DateTime.Now > expirationTime; + public bool Expired => ExpirationTime.TryUnwrap(out var expirationTime) && SerializableDateTime.LocalNow > expirationTime; public BannedPlayer( - string name, Either addressOrAccountId, string reason, DateTime? expirationTime) + string name, Either addressOrAccountId, string reason, Option expirationTime) { this.Name = name; this.AddressOrAccountId = addressOrAccountId; @@ -39,6 +39,7 @@ namespace Barotrauma.Networking { LoadBanList(); } + RemoveExpired(); } private void LoadLegacyBanList() @@ -69,7 +70,7 @@ namespace Barotrauma.Networking { if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime)) { - expirationTime = parsedTime; + expirationTime = DateTime.SpecifyKind(parsedTime, DateTimeKind.Local); } else { @@ -80,15 +81,18 @@ namespace Barotrauma.Networking } string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : ""; - if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) { continue; } + var serializableExpirationTime + = expirationTime.HasValue + ? Option.Some(new SerializableDateTime(expirationTime.Value)) + : Option.None(); if (AccountId.Parse(endpointStr).TryUnwrap(out var accountId)) { - bannedPlayers.Add(new BannedPlayer(name, accountId, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, accountId, reason, serializableExpirationTime)); } else if (Address.Parse(endpointStr).TryUnwrap(out var address)) { - bannedPlayers.Add(new BannedPlayer(name, address, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, address, reason, serializableExpirationTime)); } } @@ -109,10 +113,22 @@ namespace Barotrauma.Networking var name = element.GetAttributeString("name", "")!; var reason = element.GetAttributeString("reason", "")!; - DateTime? expirationTime = DateTime.FromBinary(unchecked((long)element.GetAttributeUInt64("expirationtime", 0))); - - if (expirationTime < DateTime.Now) { expirationTime = null; } - + var expirationTime = Option.None(); + var expirationTimeStr = element.GetAttributeString("expirationtime", "")!; + + if (UInt64.TryParse(expirationTimeStr, out var binaryDateTime) && binaryDateTime > 0) + { + // Backwards compatibility: if expirationtime is stored as an int, + // convert to SerializableDateTime with local timezone because + // banlists used to assume local time + expirationTime = Option.Some( + new SerializableDateTime( + DateTime.FromBinary((long)binaryDateTime), + SerializableTimeZone.LocalTimeZone)); + } + + expirationTime = expirationTime.Fallback(SerializableDateTime.Parse(expirationTimeStr)); + if (accountId.IsNone() && address.IsNone()) { return Option.None(); } Either addressOrAccountId = accountId.TryUnwrap(out var accId) @@ -124,8 +140,7 @@ namespace Barotrauma.Networking return Option.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); } - bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement) - .OfType>().Select(o => o.Value)); + bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement).NotNone()); } private void RemoveExpired() @@ -171,14 +186,14 @@ namespace Barotrauma.Networking string logMsg = "Banned " + name; if (!string.IsNullOrEmpty(reason)) { logMsg += ", reason: " + reason; } - if (duration.HasValue) { logMsg += ", duration: " + duration.Value.ToString(); } + if (duration.HasValue) { logMsg += ", duration: " + duration.Value; } DebugConsole.Log(logMsg); - DateTime? expirationTime = null; + Option expirationTime = Option.None(); if (duration.HasValue) { - expirationTime = DateTime.Now + duration.Value; + expirationTime = Option.Some(new SerializableDateTime(DateTime.Now + duration.Value)); } bannedPlayers.Add(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); @@ -232,9 +247,10 @@ namespace Barotrauma.Networking { retVal.SetAttributeValue("address", address.StringRepresentation); } - if (bannedPlayer.ExpirationTime is { } expirationTime) + if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime)) { - retVal.SetAttributeValue("expirationtime", unchecked((ulong)expirationTime.ToBinary())); + #warning TODO: stop writing binary DateTime representation after this gets on main + retVal.SetAttributeValue("expirationtime", expirationTime.ToLocalValue().ToBinary()); } return retVal; @@ -269,11 +285,11 @@ namespace Barotrauma.Networking outMsg.WriteString(bannedPlayer.Name); outMsg.WriteUInt32(bannedPlayer.UniqueIdentifier); - outMsg.WriteBoolean(bannedPlayer.ExpirationTime != null); + outMsg.WriteBoolean(bannedPlayer.ExpirationTime.IsSome()); outMsg.WritePadBits(); - if (bannedPlayer.ExpirationTime != null) + if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime)) { - double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; + double hoursFromNow = (expirationTime.ToUtcValue() - DateTime.UtcNow).TotalHours; outMsg.WriteDouble(hoursFromNow); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 030c04129..5ec411eff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -58,6 +58,7 @@ namespace Barotrauma.Networking private DateTime roundStartTime; + private bool wasReadyToStartAutomatically; private bool autoRestartTimerRunning; private float endRoundTimer; @@ -139,6 +140,7 @@ namespace Barotrauma.Networking ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); KarmaManager.SelectPreset(ServerSettings.KarmaPreset); ServerSettings.SetPassword(password); + ServerSettings.SaveSettings(); Voting = new Voting(); @@ -366,8 +368,7 @@ namespace Barotrauma.Networking character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); - + Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); @@ -504,8 +505,7 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && - (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) + else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame) { if (ServerSettings.AutoRestart) { @@ -526,18 +526,25 @@ namespace Barotrauma.Networking } } + bool readyToStartAutomatically = false; if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) { - StartGame(); + readyToStartAutomatically = true; } else if (ServerSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) { - StartGame(); + readyToStartAutomatically = true; } } + if (readyToStartAutomatically) + { + if (!wasReadyToStartAutomatically) { GameMain.NetLobbyScreen.LastUpdateID++; } + TryStartGame(); + } + wasReadyToStartAutomatically = readyToStartAutomatically; } for (int i = disconnectedClients.Count - 1; i >= 0; i--) @@ -763,7 +770,7 @@ namespace Barotrauma.Networking else { string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); @@ -779,7 +786,10 @@ namespace Barotrauma.Networking SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) + { + MultiPlayerCampaign.LoadCampaign(saveName); + } } break; case ClientPacketHeader.VOICE: @@ -1106,6 +1116,7 @@ namespace Barotrauma.Networking { //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } + MissionAction.NotifyMissionsUnlockedThisRound(c); c.InGame = true; } } @@ -1389,13 +1400,13 @@ namespace Barotrauma.Networking if (end) { if (mpCampaign == null || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) + CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound)) { bool save = inc.ReadBoolean(); + bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); @@ -1409,6 +1420,14 @@ namespace Barotrauma.Networking } EndGame(wasSaved: save); } + else if (mpCampaign != null) + { + Log($"Client \"{ClientLogName(sender)}\" quit the currently active campaign.", ServerLog.MessageType.ServerMessage); + GameMain.GameSession = null; + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + + } } } else @@ -1425,12 +1444,11 @@ namespace Barotrauma.Networking { MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); } - } else if (!GameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - StartGame(); + TryStartGame(); } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { @@ -1492,20 +1510,10 @@ namespace Barotrauma.Networking case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; - Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); - - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") + Log("Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ?? "none"), ServerLog.MessageType.ServerMessage); + if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign) { - const int MaxSaves = 255; - var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); - for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) - { - msg.WriteNetSerializableStruct(saveInfos[i]); - } - serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); + TrySendCampaignSetupInfo(sender); } break; case ClientPermissions.ManageCampaign: @@ -1612,7 +1620,7 @@ namespace Barotrauma.Networking { if (GameSettings.CurrentConfig.VerboseLogging) { - DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); + DebugConsole.NewMessage($"Sending initial lobby update to {c.Name}", Color.Gray); } outmsg.WriteByte(c.SessionId); @@ -1935,6 +1943,13 @@ namespace Barotrauma.Networking { outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); } + + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign && + connectedClients.None(c => c.Connection == OwnerConnection || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign))) + { + //if no-one has permissions to manage the campaign, show the setup UI to everyone + TrySendCampaignSetupInfo(c); + } } else { @@ -1944,9 +1959,8 @@ namespace Barotrauma.Networking settingsBytes = outmsg.LengthBytes - settingsBytes; int campaignBytes = outmsg.LengthBytes; - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.WriteBoolean(true); outmsg.WritePadBits(); @@ -2024,7 +2038,7 @@ namespace Barotrauma.Networking } } - private void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) + private static void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2038,10 +2052,26 @@ namespace Barotrauma.Networking } } - public bool StartGame() + public bool TryStartGame() { if (initiatedStartGame || GameStarted) { return false; } + GameModePreset selectedMode = + Voting.HighestVoted(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode; + if (selectedMode == null) + { + return false; + } + if (selectedMode == GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is not MultiPlayerCampaign) + { + //DebugConsole.ThrowError($"{nameof(TryStartGame)} failed. Cannot start a multiplayer campaign via {nameof(TryStartGame)} - use {nameof(MultiPlayerCampaign.StartNewCampaign)} or {nameof(MultiPlayerCampaign.LoadCampaign)} instead."); + if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.MultiPlayerCampaign.Identifier; + } + return false; + } + Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; @@ -2061,23 +2091,13 @@ namespace Barotrauma.Networking return false; } - GameModePreset selectedMode = Voting.HighestVoted(VoteType.Mode, connectedClients); - if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; } - if (selectedMode == null) - { - return false; - } - if (selectedMode == GameModePreset.MultiPlayerCampaign && !(GameMain.GameSession?.GameMode is CampaignMode)) - { - DebugConsole.ThrowError("StartGame failed. Cannot start a multiplayer campaign via StartGame - use MultiPlayerCampaign.StartNewCampaign or MultiPlayerCampaign.LoadCampaign instead."); - return false; - } initiatedStartGame = true; - startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); + startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); return true; } + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -2169,7 +2189,6 @@ namespace Barotrauma.Networking initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true }; } - List playingClients = new List(connectedClients); if (ServerSettings.AllowSpectating) { @@ -2414,8 +2433,7 @@ namespace Barotrauma.Networking mpCampaign.ClearSavedExperiencePoints(teamClients[i]); } - spawnedCharacter.OwnerClientAddress = teamClients[i].Connection.Endpoint.Address; - spawnedCharacter.OwnerClientName = teamClients[i].Name; + spawnedCharacter.SetOwnerClient(teamClients[i]); } for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) @@ -2479,7 +2497,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); GameMain.GameScreen.Select(); @@ -2507,14 +2525,11 @@ namespace Barotrauma.Networking private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) { - MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.STARTGAME); msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); - bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); @@ -2530,7 +2545,7 @@ namespace Barotrauma.Networking ServerSettings.WriteMonsterEnabled(msg); - if (campaign == null) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { msg.WriteString(levelSeed); msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); @@ -2566,6 +2581,23 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } + private bool TrySendCampaignSetupInfo(Client client) + { + if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } + + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + { + msg.WriteNetSerializableStruct(saveInfos[i]); + } + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + return true; + } + private bool IsUsingRespawnShuttle() { return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); @@ -2947,7 +2979,7 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (client.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -3196,16 +3228,16 @@ namespace Barotrauma.Networking } //too far to hear the msg -> don't send - if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; + if (string.IsNullOrWhiteSpace(modifiedMessage)) { continue; } } break; case ChatMessageType.Dead: //character still alive -> don't send - if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; + if (client != senderClient && client.Character != null && !client.Character.IsDead) { continue; } break; case ChatMessageType.Private: //private msg sent to someone else than this client -> don't send - if (client != targetClient && client != senderClient) continue; + if (client != targetClient && client != senderClient) { continue; } break; } @@ -3241,11 +3273,17 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio)) + { + //send to chat-linked wifi components + Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item); + senderRadio.TransmitSignal(s, sentFromChat: true); + } } } @@ -3350,12 +3388,11 @@ namespace Barotrauma.Networking public void SwitchSubmarine() { - if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; } + if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; } SubmarineInfo targetSubmarine = subVote.Sub; VoteType voteType = Voting.ActiveVote.VoteType; Client starter = Voting.ActiveVote.VoteStarter; - int deliveryFee = 0; switch (voteType) { @@ -3365,7 +3402,6 @@ namespace Barotrauma.Networking GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: - deliveryFee = subVote.DeliveryFee; break; default: return; @@ -3373,7 +3409,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); } Voting.StopSubmarineVote(true); @@ -3521,9 +3557,7 @@ namespace Barotrauma.Networking //the client's previous character is no longer a remote player if (client.Character != null) { - client.Character.IsRemotePlayer = false; - client.Character.OwnerClientAddress = null; - client.Character.OwnerClientName = null; + client.Character.SetOwnerClient(null); } if (newCharacter == null) @@ -3549,9 +3583,7 @@ namespace Barotrauma.Networking newCharacter.Info.Character = newCharacter; } - newCharacter.OwnerClientAddress = client.Connection.Endpoint.Address; - newCharacter.OwnerClientName = client.Name; - newCharacter.IsRemotePlayer = true; + newCharacter.SetOwnerClient(client); newCharacter.Enabled = true; client.Character = newCharacter; CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); @@ -3939,8 +3971,7 @@ namespace Barotrauma.Networking } public void Quit() - { - + { if (started) { started = false; @@ -3952,7 +3983,7 @@ namespace Barotrauma.Networking ServerSettings.SaveSettings(); - ModSender.Dispose(); + ModSender?.Dispose(); if (ServerSettings.SaveServerLogs) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e732ce117..737e9555d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -158,7 +158,7 @@ namespace Barotrauma else if (client.Karma < 40.0f) herpesStrength = 30.0f; - var existingAffliction = client.Character.CharacterHealth.GetAffliction("spaceherpes"); + var existingAffliction = client.Character.CharacterHealth.GetAffliction(AfflictionPrefab.SpaceHerpesType); if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f); } } @@ -358,8 +358,8 @@ namespace Barotrauma } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; + bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; //huskified characters count as enemies to healthy characters and vice versa if (targetIsHusk != attackerIsHusk) { isEnemy = true; } @@ -614,7 +614,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index a02d01ebe..600503651 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteString(Text); msg.WriteString(SenderName); msg.WriteBoolean(SenderClient != null); if (SenderClient != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index b231a8672..b60d34661 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId); + PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId); DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}"); if (pendingClient is null) @@ -306,7 +306,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { return; } if (connectedClients.Find(c - => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) is LidgrenConnection connection) { Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); @@ -380,7 +380,7 @@ namespace Barotrauma.Networking lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (conn.AccountInfo.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } } lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 302b5b959..b8e4393bf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking protected List connectedClients = null!; protected List pendingClients = null!; protected ServerSettings serverSettings = null!; - protected Option ownerKey = null!; + protected Option ownerKey = Option.None; protected NetworkConnection? OwnerConnection; protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) @@ -246,7 +246,7 @@ namespace Barotrauma.Networking { case ConnectionInitialization.ContentPackageOrder: - DateTime timeNow = DateTime.UtcNow; + SerializableDateTime timeNow = SerializableDateTime.UtcNow; structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, @@ -290,7 +290,7 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some { Value: SteamId steamId }) + if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap(out var steamId)) { Steam.SteamManager.StopAuthSession(steamId); pendingClient.Connection.SetAccountInfo(AccountInfo.None); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 4f93198d9..bc268f9dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -218,7 +218,10 @@ namespace Barotrauma.Networking foreach (Door door in shuttleDoors) { - if (door.IsOpen) door.TrySetState(false, false, true); + if (door.IsOpen) + { + door.TrySetState(open: false, isNetworkMessage: false, sendNetworkMessage: true); + } } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); @@ -439,8 +442,7 @@ namespace Barotrauma.Networking } clients[i].Character = character; - character.OwnerClientAddress = clients[i].Connection.Endpoint.Address; - character.OwnerClientName = clients[i].Name; + character.SetOwnerClient(clients[i]); GameServer.Log( $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 7e348523a..d3ef072bb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); - + partial void InitProjSpecific() { LoadSettings(); @@ -176,7 +176,11 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log( + NetworkMember.ClientLogName(c) + + $" changed {netProperties[key].Name}" + + $" to {netProperties[key].Value}", + ServerLog.MessageType.ServerMessage); } propertiesChanged = true; } @@ -330,6 +334,10 @@ namespace Barotrauma.Networking { LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } + if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", ""))) + { + Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index be597c2da..53c3b7eda 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -28,13 +28,11 @@ namespace Barotrauma public SubmarineInfo Sub; public bool TransferItems; - public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType) { Sub = subInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; VoteStarter = starter; @@ -109,7 +107,6 @@ namespace Barotrauma sender, subInfo, transferItems, - voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); @@ -117,13 +114,13 @@ namespace Barotrauma public void StopSubmarineVote(bool passed) { - if (!(ActiveVote is SubmarineVote)) { return; } + if (ActiveVote is not SubmarineVote) { return; } StopActiveVote(passed); } public void StopMoneyTransferVote(bool passed) { - if (!(ActiveVote is TransferVote)) { return; } + if (ActiveVote is not TransferVote) { return; } StopActiveVote(passed); } @@ -155,7 +152,7 @@ namespace Barotrauma GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } - private void StartOrEnqueueVote(IVote vote) + private static void StartOrEnqueueVote(IVote vote) { if (ActiveVote == null) { @@ -198,9 +195,9 @@ namespace Barotrauma ActiveVote.Timer += deltaTime; - if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout || inGameClients.Count() == 1) { - 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 @@ -216,7 +213,7 @@ namespace Barotrauma } } - public void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) + public static void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) { foreach (Client client in connectedClients) { @@ -254,7 +251,14 @@ namespace Barotrauma string modeIdentifier = inc.ReadString(); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); if (mode == null || !mode.Votable) { break; } + var prevHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); sender.SetVote(voteType, mode); + var newHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); + if (prevHighestVoted != newHighestVoted) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = mode.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + } break; case VoteType.EndRound: if (!sender.HasSpawned) { return; } @@ -429,7 +433,6 @@ namespace Barotrauma var subVote = ActiveVote as SubmarineVote; msg.WriteString(subVote.Sub.Name); msg.WriteBoolean(subVote.TransferItems); - msg.WriteInt16((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index ec2ca5d40..ff3f08bbe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -59,6 +59,7 @@ namespace Barotrauma get { return GameModes[SelectedModeIndex].Identifier; } set { + if (SelectedModeIdentifier == value) { return; } for (int i = 0; i < GameModes.Length; i++) { if (GameModes[i].Identifier == value) @@ -127,9 +128,11 @@ namespace Barotrauma { LevelSeed = ToolBox.RandomSeed(8); - subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)).ToList(); + subs = SubmarineInfo.SavedSubmarines + .Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)) + .ToList(); - if (subs == null || subs.Count() == 0) + if (subs == null || subs.Count == 0) { throw new Exception("No submarines are available."); } @@ -156,7 +159,7 @@ namespace Barotrauma GameModes = GameModePreset.List.ToArray(); } - private List subs; + private readonly List subs; public IReadOnlyList GetSubList() => subs; public string LevelSeed @@ -192,7 +195,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; @@ -201,20 +204,24 @@ namespace Barotrauma public void RandomizeSettings() { - if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8); + if (GameMain.Server.ServerSettings.RandomizeSeed) { LevelSeed = ToolBox.RandomSeed(8); } - if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + //don't touch any of these settings if a campaign is running! + if (GameMain.GameSession?.Campaign == null) { - var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); - SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; - } - if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) - { - var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); - SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; - } + if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + { + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); + SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; + } + if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) + { + var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); + SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; + } - GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 18e6c142d..eaa70b999 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -71,6 +71,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); + Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d03e7cda3..ef40122b9 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.13.0.0 + 1.0.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -29,7 +29,6 @@ full true - TRACE;SERVER;WINDOWS;USE_STEAM diff --git a/Barotrauma/BarotraumaShared/Data/languageoptions.xml b/Barotrauma/BarotraumaShared/Data/languageoptions.xml new file mode 100644 index 000000000..f1fdbe04b --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/languageoptions.xml @@ -0,0 +1,22 @@ + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index bd7347e70..131be5ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -352,21 +352,6 @@ namespace Barotrauma } } - public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) - { - if (myTeam == otherTeam) { return true; } - return myTeam switch - { - // NPCs are friendly to the same team and the friendly NPCs - CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, - // Friendly NPCs are friendly to both player teams - CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, - _ => true - }; - } - - public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); - public void ReequipUnequipped() { foreach (var item in unequippedItems) @@ -460,7 +445,7 @@ namespace Barotrauma if (EscapeTarget != null) { var door = EscapeTarget.ConnectedDoor; - bool isClosedDoor = door != null && !door.IsOpen; + bool isClosedDoor = door != null && door.IsClosed; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 449ec68dd..156545868 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -206,13 +206,19 @@ namespace Barotrauma private set; } = new HashSet(); - public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget); public static bool IsTargetBeingChasedBy(Character target, Character character) => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive); public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); - private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + + private bool IsAttackingOwner(Character other) => + PetBehavior != null && PetBehavior.Owner != null && + !other.IsUnconscious && !other.IsArrested && + other.AIController is HumanAIController humanAI && + humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && + combat.Enemy != null && combat.Enemy == PetBehavior.Owner; private bool reverse; public bool Reverse @@ -308,11 +314,10 @@ namespace Barotrauma insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; - requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); myBodies.Add(Character.AnimController.Collider.FarseerBody); + CreatureMetrics.UnlockInEditor(Character.SpeciesName); } private CharacterParams.AIParams _aiParams; @@ -354,7 +359,7 @@ namespace Barotrauma { targetingTag = "owner"; } - else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter)) + else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) { targetingTag = "hostile"; } @@ -683,19 +688,22 @@ namespace Barotrauma { if (SelectedAiTarget.Entity is Character targetCharacter) { - bool IsValid(Character.Attacker a) + bool ShouldRetaliate(Character.Attacker a) { Character c = a.Character; - if (c.IsDead || c.Removed) { return false; } - if (!Character.IsFriendly(c)) { return true; } - if (!c.IsPlayer) { return false; } - // Only apply the threshold to players - return a.Damage >= selectedTargetingParams.Threshold; + if (c == null || c.IsUnconscious || c.Removed) { return false; } + // Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group. + if (Character.IsSameSpeciesOrGroup(c)) { return false; } + if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; } + if (c.IsPlayer || Character.IsOnFriendlyTeam(c)) + { + return a.Damage >= selectedTargetingParams.Threshold; + } + return true; } - Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; - if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker)) + Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character; + if (attacker?.AiTarget != null) { - // Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group. ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); State = AIState.Attack; @@ -869,7 +877,7 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { - if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f) { // Steer straight up if very deep SteeringManager.SteeringManual(deltaTime, Vector2.UnitY); @@ -1503,7 +1511,7 @@ namespace Barotrauma { hitTarget = limb.character; } - if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget)) { return true; } @@ -2413,7 +2421,7 @@ namespace Barotrauma { t = limb.character; } - if (t != null && (t == target || !Character.IsFriendly(t))) + if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t)))) { return true; } @@ -3062,7 +3070,8 @@ namespace Barotrauma // In the attack state allow going into non-allowed zone only when chasing a target. if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } } - if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) + bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine; + if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) { // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state. bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 1a6ff9f52..ab2800893 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -310,7 +310,7 @@ namespace Barotrauma UseIndoorSteeringOutside = false; } - if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) + if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior @@ -444,7 +444,7 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; + bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) @@ -541,12 +541,12 @@ namespace Barotrauma if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } - bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen; + bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1); bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { - if (!Character.NeedsAir) { return false; } + if (Character.IsImmuneToPressure) { return false; } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); return gotoObjective.Target != null && targetHull == null || @@ -566,17 +566,17 @@ namespace Barotrauma gotoObjective.Abandon = true; } } - if (!oxygenLow) + if (!shouldActOnSuffocation) { return; } } // Diving gear - if (oxygenLow || findItemState != FindItemState.OtherItem) + if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem) { bool needsGear = NeedsDivingGear(Character.CurrentHull, out _); - if (!needsGear || oxygenLow) + if (!needsGear || shouldActOnSuffocation) { bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective(); bool shouldKeepTheGearOn = @@ -584,21 +584,21 @@ namespace Barotrauma Character.AnimController.InWater || Character.AnimController.HeadInWater || Character.Submarine == null || - (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || + (!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || Character.CurrentHull.IsWetRoom; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); - if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) + if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) { shouldKeepTheGearOn = false; // Remove the suit before we pass out removeDivingSuit = true; } bool takeMaskOff = !shouldKeepTheGearOn; - if (!shouldKeepTheGearOn && !oxygenLow) + if (!shouldKeepTheGearOn && !shouldActOnSuffocation) { if (ObjectiveManager.IsCurrentObjective()) { @@ -621,9 +621,10 @@ namespace Barotrauma } else if (gotoObjective.Mimic) { + bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false); if (!removeSuit) { - removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + removeDivingSuit = !targetHasDivingGear; if (removeDivingSuit) { removeSuit = true; @@ -631,7 +632,7 @@ namespace Barotrauma } if (!removeMask) { - takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + takeMaskOff = !targetHasDivingGear; if (takeMaskOff) { removeMask = true; @@ -647,7 +648,7 @@ namespace Barotrauma var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) { - if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); HandleRelocation(divingSuit); @@ -783,20 +784,23 @@ namespace Barotrauma private void HandleRelocation(Item item) { - if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC) + if (item.SpawnedInCurrentOutpost) { return; } + if (item.Submarine == null) { return; } + // Only affects bots in the player team + if (!Character.IsOnPlayerTeam) { return; } + // Don't relocate if the item is on a sub of the same team + if (item.Submarine.TeamID == Character.TeamID) { return; } + if (itemsToRelocate.Contains(item)) { return; } + itemsToRelocate.Add(item); + if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) { - if (itemsToRelocate.Contains(item)) { return; } - itemsToRelocate.Add(item); - if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) - { - myPort.OnUnDocked += Relocate; - } - var campaign = GameMain.GameSession.Campaign; - if (campaign != null) - { - // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. - campaign.BeforeLevelLoading += Relocate; - } + myPort.OnUnDocked += Relocate; + } + var campaign = GameMain.GameSession.Campaign; + if (campaign != null) + { + // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. + campaign.BeforeLevelLoading += Relocate; } void Relocate() @@ -982,15 +986,15 @@ namespace Barotrauma if (target.CurrentHull != hull) { continue; } if (AIObjectiveRescueAll.IsValidTarget(target, Character)) { - if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + if (AddTargets(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } - } - foreach (Item item in Item.ItemList) + } + foreach (Item item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) @@ -1161,7 +1165,7 @@ namespace Barotrauma freezeAI = true; } } - if (attacker == null || attacker.IsDead || attacker.Removed) + if (attacker == null || attacker.IsUnconscious || attacker.Removed) { // Don't react to the damage if there's no attacker. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. @@ -1199,7 +1203,7 @@ namespace Barotrauma return; } float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); - bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; + bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) @@ -1209,7 +1213,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1279,7 +1283,7 @@ namespace Barotrauma if (otherCharacter.Submarine != attacker.Submarine) { continue; } if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } if (otherCharacter.IsPlayer) { continue; } - if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } + if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); if (!isWitnessing) @@ -1299,7 +1303,7 @@ namespace Barotrauma AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false) { - if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; } + if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; } if (!IsFriendly(attacker)) { if (c.Submarine == null) @@ -1327,7 +1331,7 @@ namespace Barotrauma } if (attacker.IsPlayer && c.TeamID == attacker.TeamID) { - if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID) + if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID) { // Bots in the player team never act aggressively in single player when attacked by the player // In multiplayer, they react only to players attacking them or other crew members @@ -1345,11 +1349,11 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } - if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + if (isWitnessing && c.CombatAction != null && !c.IsSecurity) { - return Character.CombatAction.WitnessReaction; + return c.CombatAction.WitnessReaction; } - if (attacker.IsPlayer && FindInstigator() is Character instigator) + if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator) { // The guards don't react to player's aggressions when there's an instigator around isAttackerFightingEnemy = true; @@ -1359,11 +1363,11 @@ namespace Barotrauma { if (c.IsSecurity) { - return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None; + return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive; } else { - return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None; + return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; } } else @@ -1546,7 +1550,7 @@ namespace Barotrauma public bool NeedsDivingGear(Hull hull, out bool needsSuit) { - if (!Character.NeedsAir) + if (Character.IsImmuneToPressure) { needsSuit = false; return false; @@ -1557,30 +1561,30 @@ namespace Barotrauma hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); + needsSuit = true; return true; } - if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) + if (Character.CharacterHealth.OxygenLowResistance < 1 && (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)) { return true; } return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); + public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0) - => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true, + public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true, predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes)); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// - public static bool HasDivingMask(Character character, float conditionPercentage = 0) - => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); + public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); @@ -1745,7 +1749,9 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound && + Level.Loaded?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, @@ -1843,7 +1849,7 @@ namespace Barotrauma } break; case "reportbrokendevices": - foreach (var item in Item.ItemList) + foreach (var item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) @@ -1924,11 +1930,12 @@ namespace Barotrauma bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { + float hullSafety = character.IsProtectedFromPressure ? 0 : 100; if (isCurrentHull) { - CurrentHullSafety = character.NeedsAir ? 0 : 100; + CurrentHullSafety = hullSafety; } - return CurrentHullSafety; + return hullSafety; } if (isCurrentHull && visibleHulls == null) { @@ -1936,10 +1943,9 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = character.IsProtectedFromPressure(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = character.IsProtectedFromPressure || HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); - float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { CurrentHullSafety = safety; @@ -1949,15 +1955,33 @@ namespace Barotrauma private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { - if (hull == null) { return character.NeedsAir ? 0 : 100; } - if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; } + bool isProtectedFromPressure = character.IsProtectedFromPressure; + if (hull == null) { return isProtectedFromPressure ? 100 : 0; } + if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; } // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); - float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100); - if (!character.NeedsAir) + float waterFactor = 1; + if (!ignoreWater) + { + if (visibleHulls != null) + { + // Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results. + float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume); + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + else + { + float relativeWaterVolume = hull.WaterVolume / hull.Volume; + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + } + if (character.CharacterHealth.OxygenLowResistance >= 1) { oxygenFactor = 1; + } + if (isProtectedFromPressure) + { waterFactor = 1; } float fireFactor = 1; @@ -2045,21 +2069,39 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other); + bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } - if (!me.IsSameSpeciesOrGroup(other)) { return false; } - if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) - { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) - { - return false; - } - } - if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) + if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. - return false; + if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + } + else + { + if (!me.IsSameSpeciesOrGroup(other)) { return false; } + } + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || + (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) + { + Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; + Identifier npcFaction = npc.Faction; + Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; + if (npcFaction.IsEmpty) + { + //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost + npcFaction = currentLocationFaction; + } + if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) + { + var reputation = campaign.Map?.CurrentLocation?.Reputation; + if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + { + return false; + } + } + } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 9c3b8c40b..73db418cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -96,7 +96,7 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen) { buttonPressTimer = 0; } @@ -211,7 +211,7 @@ namespace Barotrauma currentTarget = target; Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; - pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; + pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) @@ -342,7 +342,7 @@ namespace Barotrauma CheckDoorsInPath(); doorsChecked = true; } - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && lastDoor.door.IsOpening) { // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. Reset(); @@ -510,7 +510,7 @@ namespace Barotrauma private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsBroken) { return true; } - if (!door.IsOpen) + if (door.IsClosed) { if (!door.Item.IsInteractable(character)) { return false; } if (!ShouldBreakDoor(door)) @@ -536,7 +536,7 @@ namespace Barotrauma } foreach (var linked in door.Item.linkedTo) { - if (!(linked is Item linkedItem)) { continue; } + if (linked is not Item linkedItem) { continue; } var button = linkedItem.GetComponent(); if (button == null) { continue; } if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) @@ -785,7 +785,7 @@ namespace Barotrauma { if (hull.WaterVolume / hull.Rect.Width > 100.0f) { - if (!HumanAIController.HasDivingSuit(character)) + if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1) { penalty += 500.0f; } @@ -808,7 +808,7 @@ namespace Barotrauma private float? GetSingleNodePenalty(PathNode node) { - if (node.Waypoint.isObstructed) { return null; } + if (!node.Waypoint.IsTraversable) { return null; } if (node.IsBlocked()) { return null; } float penalty = 0.0f; if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index e277590bd..5eb1bb55f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -352,7 +352,7 @@ namespace Barotrauma Weapon = null; continue; } - if (WeaponComponent.IsLoaded(character)) + if (WeaponComponent.IsNotEmpty(character)) { // All good, the weapon is loaded break; @@ -470,7 +470,7 @@ namespace Barotrauma // Not in the inventory anymore or cannot find the weapon component return false; } - if (!WeaponComponent.IsLoaded(character)) + if (!WeaponComponent.IsNotEmpty(character)) { // Try reloading (and seek ammo) if (!Reload(seekAmmo)) @@ -541,7 +541,7 @@ namespace Barotrauma priority /= 2; } } - if (!weapon.IsLoaded(character)) + if (!weapon.IsNotEmpty(character)) { if (weapon is RangedWeapon && !isAllowedToSeekWeapons) { @@ -554,7 +554,15 @@ namespace Barotrauma priority /= 2; } } - if (Enemy.IsKnockedDown) + + if (Enemy.Params.Health.StunImmunity) + { + if (weapon.Item.HasTag("stunner")) + { + priority /= 2; + } + } + else if (Enemy.IsKnockedDown) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); @@ -640,11 +648,11 @@ namespace Barotrauma { statusEffects = statusEffects.Concat(hitEffects); } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0); + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => { float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun"); + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); if (stunAffliction != null) { stunAmount = stunAffliction.Strength; @@ -1168,30 +1176,31 @@ namespace Barotrauma if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); - if (pickedBody != null) + // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + foreach (var body in pickedBodies) { Character target = null; - if (pickedBody.UserData is Character c) + if (body.UserData is Character c) { target = c; } - else if (pickedBody.UserData is Limb limb) + else if (body.UserData is Limb limb) { target = limb.character; } - if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) + if (target != null && (target != Enemy || HumanAIController.IsFriendly(target))) { - UseWeapon(deltaTime); + return; } } + UseWeapon(deltaTime); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 765b8de83..4c1d874d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -200,7 +201,8 @@ namespace Barotrauma (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, - SpeakIfFails = !objectiveManager.IsCurrentOrder() + SpeakIfFails = !objectiveManager.IsCurrentOrder(), + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); @@ -244,7 +246,8 @@ namespace Barotrauma public bool IsInTargetSlot(Item item) { - if (container?.Inventory is ItemInventory inventory && TargetSlot is not null) + if (TargetSlot == null) { return true; } + if (container?.Inventory is ItemInventory inventory) { return inventory.IsInSlot(item, (int)TargetSlot); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 5dcbdb17e..aee20f6ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -19,7 +19,6 @@ namespace Barotrauma private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGoTo gotoObjective; - private float useExtinquisherTimer; public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -44,7 +43,8 @@ namespace Barotrauma } else { - float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 3 : 0; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); @@ -119,24 +119,18 @@ namespace Barotrauma Abandon = true; break; } - float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; - float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); - bool inRange = xDist + yDist < extinguisher.Range; - // Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall. - ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity; - bool move = !inRange || !character.CanSeeTarget(lookTarget); - if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0) + float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X); + float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y); + float dist = xDist + yDist; + bool inRange = dist < extinguisher.Range; + bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull); + bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull)); + bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); + if (operateExtinguisher) { - useExtinquisherTimer += deltaTime; - if (useExtinquisherTimer > 2.0f) - { - useExtinquisherTimer = 0.0f; - } - // Aim character.CursorPosition = fs.Position; Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; - float dist = fromCharacterToFireSource.Length(); - character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); if (extinguisherItem.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -148,25 +142,29 @@ namespace Barotrauma { character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } + // Prevents running into the flames. + objectiveManager.CurrentObjective.ForceWalk = true; } - if (move) + if (moveCloser) { - //go to the first firesource - if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) - { - DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), - TargetName = fs.Hull.DisplayName - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref gotoObjective))) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f) + { + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), + TargetName = fs.Hull.DisplayName, + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective))) { gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } - else + else if (!operateExtinguisher || isInDamageRange) { - character.AIController.SteeringManager.Reset(); + // Don't walk into the flames. + RemoveSubObjective(ref gotoObjective); + SteeringManager.Reset(); } + // Only target one fire source at the time. break; } } @@ -177,8 +175,20 @@ namespace Barotrauma base.Reset(); getExtinguisherObjective = null; gotoObjective = null; - useExtinquisherTimer = 0; sinTime = 0; + SteeringManager.Reset(); + } + + protected override void OnCompleted() + { + base.OnCompleted(); + SteeringManager.Reset(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + SteeringManager.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 26f10fd92..d53fe6a38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -47,31 +47,25 @@ namespace Barotrauma } if (character.CurrentHull == null) { - if (!character.NeedsAir) - { - Priority = 0; - } - else - { - Priority = ( - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && HumanAIController.HasDivingSuit(character) ? 0 : 100; - } + Priority = ( + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasActiveObjective() || + objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + && character.IsProtectedFromPressure ? 0 : 100; } else { - if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && + if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || + (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && (needsSuit ? !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : - !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))) + !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))) { Priority = 100; } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && - character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) { // Ordered to follow, hold position, or return back to main sub inside a hostile sub // -> ignore find safety unless we need to find a diving gear @@ -117,6 +111,11 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; + if (currenthullSafety >= 100) + { + // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. + Priority = 0; + } } else { @@ -137,12 +136,14 @@ namespace Barotrauma private float retryTimer; protected override void Act(float deltaTime) { + if (resetPriority) { return; } var currentHull = character.CurrentHull; - bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; - if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull)) + bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); + bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); + if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); - bool needsEquipment = false; + bool needsEquipment = shouldActOnSuffocation; if (needsDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character)); @@ -218,7 +219,11 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + AllowGoingOutside = + character.IsProtectedFromPressure || + character.CurrentHull == null || + character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.LeadsOutside(character) }, onCompleted: () => { @@ -349,8 +354,8 @@ namespace Barotrauma //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()) + float hullSuitability = EstimateHullSuitability(character, hull); + if (hulls.None()) { hulls.Add(hull); } @@ -445,9 +450,12 @@ namespace Barotrauma { hullSafety = 100; } + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // 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)); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 3e5531d20..dd0b1e20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -155,17 +155,21 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), - onAbandon: () => Abandon = true, - onCompleted: () => + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak) + { + // Use an empty filter to override the default + EndNodeFilter = n => true + }, + onAbandon: () => Abandon = true, + onCompleted: () => + { + if (CheckObjectiveSpecific()) { IsCompleted = true; } + else { - if (CheckObjectiveSpecific()) { IsCompleted = true; } - else - { - // Failed to operate. Probably too far. - Abandon = true; - } - }); + // Failed to operate. Probably too far. + Abandon = true; + } + }); } else { @@ -178,7 +182,7 @@ namespace Barotrauma requiredCondition = () => Leak.Submarine == character.Submarine && Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), - endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), + endNodeFilter = IsSuitableEndNode, // 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() }, @@ -197,6 +201,14 @@ namespace Barotrauma } }, onCompleted: () => RemoveSubObjective(ref gotoObjective)); + + bool IsSuitableEndNode(PathNode n) + { + if (n.Waypoint.CurrentHull is null) { return false; } + if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; } + // Accept also nodes located in the linked hulls (multi-hull rooms) + return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 407b65230..2f3e75a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -54,7 +54,7 @@ namespace Barotrauma public bool AllowVariants { get; set; } public bool Equip { get; set; } public bool Wear { get; set; } - public bool RequireLoaded { get; set; } + public bool RequireNonEmpty { get; set; } public bool EvaluateCombatPriority { get; set; } public bool CheckPathForEachItem { get; set; } public bool SpeakIfFails { get; set; } @@ -123,6 +123,11 @@ namespace Barotrauma return ignoredTags; } + public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) + { + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + } + private bool CheckInventory() { if (IdentifiersOrTags == null) { return false; } @@ -155,11 +160,6 @@ namespace Barotrauma Abandon = true; return; } - if (character.Submarine == null) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -171,9 +171,14 @@ namespace Barotrauma } if (!isDoneSeeking) { + if (character.Submarine == null) + { + Abandon = true; + return; + } if (!AllowDangerousPressure) { - bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0; + bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0); if (dangerousPressure) { #if DEBUG @@ -192,6 +197,11 @@ namespace Barotrauma return; } } + else if (character.Submarine == null) + { + Abandon = true; + return; + } if (targetItem == null || targetItem.Removed) { #if DEBUG @@ -307,7 +317,8 @@ namespace Barotrauma { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, - SpeakIfFails = false + SpeakIfFails = false, + endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, onAbandon: () => @@ -391,10 +402,10 @@ namespace Barotrauma { if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } } - float itemPriority = 1; + float itemPriority = item.Prefab.BotPriority; if (GetItemPriority != null) { - itemPriority = GetItemPriority(item); + itemPriority *= GetItemPriority(item); } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) @@ -513,7 +524,7 @@ namespace Barotrauma float lowestCost = float.MaxValue; foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { - if (!(prefab is ItemPrefab itemPrefab)) { continue; } + if (prefab is not ItemPrefab itemPrefab) { continue; } if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id))) { float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ? @@ -561,7 +572,7 @@ namespace Barotrauma if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } - if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } + if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; } return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index e6d81bd12..5355767d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -21,7 +21,7 @@ namespace Barotrauma public bool CheckInventory { get; set; } public bool EvaluateCombatPriority { get; set; } public bool CheckPathForEachItem { get; set; } - public bool RequireLoaded { get; set; } + public bool RequireNonEmpty { get; set; } public bool RequireAllItems { get; set; } private readonly ImmutableArray gearTags; @@ -61,7 +61,7 @@ namespace Barotrauma AllowStealing = AllowStealing, ignoredIdentifiersOrTags = ignoredTags, CheckPathForEachItem = CheckPathForEachItem, - RequireLoaded = RequireLoaded, + RequireNonEmpty = RequireNonEmpty, ItemCount = count, SpeakIfFails = RequireAllItems }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 2c50d87ed..e4afa8c54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -50,6 +50,7 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; + private bool cantFindDivingGear; /// /// Display units @@ -90,7 +91,7 @@ namespace Barotrauma /// public bool UseDistanceRelativeToAimSourcePos { get; set; } = false; - public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; @@ -263,48 +264,73 @@ namespace Barotrauma } if (!Abandon) { - if (getDivingGearIfNeeded && !character.LockHands) + if (getDivingGearIfNeeded) { Character followTarget = Target as Character; - bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - if (Mimic) + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; + bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool tryToGetDivingSuit = needsDivingSuit; + if (Mimic && !character.IsImmuneToPressure) { if (HumanAIController.HasDivingSuit(followTarget)) { - needsDivingGear = true; - needsDivingSuit = true; + tryToGetDivingGear = true; + tryToGetDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget)) + else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) { - needsDivingGear = true; + tryToGetDivingGear = true; } } bool needsEquipment = false; float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); - if (needsDivingSuit) + if (tryToGetDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } - else if (needsDivingGear) + else if (tryToGetDivingGear) { needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } - if (needsEquipment) + if (character.LockHands) + { + cantFindDivingGear = true; + } + if (cantFindDivingGear && needsDivingSuit) + { + // Don't try to reach the target without a suit because it's lethal. + Abandon = true; + return; + } + if (needsEquipment && !cantFindDivingGear) { SteeringManager.Reset(); - if (findDivingGear != null && !findDivingGear.CanBeCompleted) - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } - else - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), + onAbandon: () => + { + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); + } + }, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 72afb8181..3625f7a1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -170,7 +170,8 @@ namespace Barotrauma TargetHull = character.CurrentHull; } - if (behavior == BehaviorType.StayInHull) + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -190,9 +191,6 @@ namespace Barotrauma } else { - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || - (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (currentTarget != null && !currentTargetIsInvalid) { if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 652ce82a5..142a57783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -364,8 +364,7 @@ namespace Barotrauma CurrentOrders.RemoveAt(i); continue; } - var currentOrderInfo = character.GetCurrentOrder(currentOrder); - if (currentOrderInfo is Order) + if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo) { int currentPriority = currentOrderInfo.ManualPriority; if (currentOrder.ManualPriority != currentPriority) @@ -539,7 +538,8 @@ namespace Barotrauma KeepActiveWhenReady = true, CheckInventory = true, Equip = false, - FindAllItems = true + FindAllItems = true, + RequireNonEmpty = false }; break; case "findweapon": @@ -555,7 +555,8 @@ namespace Barotrauma KeepActiveWhenReady = false, CheckInventory = false, EvaluateCombatPriority = true, - FindAllItems = false + FindAllItems = false, + RequireNonEmpty = true }; } prepareObjective.KeepActiveWhenReady = false; @@ -600,9 +601,9 @@ namespace Barotrauma Order dismissOrder = currentOrder.GetDismissal(); #if CLIENT - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) + if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer) { - GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder); + character.SetOrder(dismissOrder, isNewOrder: true, speak: false); } #else GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 5a8c8bbd6..06305e134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -23,6 +23,11 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; + /// + /// If undefined, a default filter will be used. + /// + public Func EndNodeFilter; + public bool Override { get; set; } = true; public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); @@ -232,7 +237,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { TargetName = target.Item.Name, - endNodeFilter = node => node.Waypoint.Ladders == null + endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index d83f75768..de55f4035 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -27,6 +27,7 @@ namespace Barotrauma public bool FindAllItems { get; set; } public bool Equip { get; set; } public bool EvaluateCombatPriority { get; set; } + public bool RequireNonEmpty { get; set; } private AIObjective GetSubObjective() { @@ -74,7 +75,7 @@ namespace Barotrauma Abandon = true; } - else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character)))) + else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character)))) { Reset(); } @@ -106,7 +107,7 @@ namespace Barotrauma CheckInventory = CheckInventory, Equip = Equip, EvaluateCombatPriority = EvaluateCombatPriority, - RequireLoaded = true, + RequireNonEmpty = RequireNonEmpty, RequireAllItems = requireAll }, onCompleted: () => @@ -157,7 +158,7 @@ namespace Barotrauma { EvaluateCombatPriority = EvaluateCombatPriority, SpeakIfFails = true, - RequireLoaded = true + RequireNonEmpty = RequireNonEmpty }; } if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index e8b5dea26..9c06fb8b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -17,7 +17,6 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem refuelObjective; - private float previousCondition = -1; private RepairTool repairTool; private const float WaitTimeBeforeRepair = 0.5f; @@ -196,15 +195,7 @@ namespace Barotrauma Abandon = true; } } - if (previousCondition == -1) - { - previousCondition = Item.Condition; - } - else if (Item.Condition < previousCondition) - { - // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. - Abandon = true; - } + CheckPreviousCondition(deltaTime); } if (Abandon) { @@ -229,7 +220,6 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => { - previousCondition = -1; var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { TargetName = Item.Name @@ -251,6 +241,27 @@ namespace Barotrauma } } + private const float conditionCheckDelay = 1; + private float conditionCheckTimer; + private float previousCondition; + private void CheckPreviousCondition(float deltaTime) + { + if (Item == null || Item.Removed) { return; } + conditionCheckTimer -= deltaTime; + if (conditionCheckTimer > 0) { return; } + conditionCheckTimer = conditionCheckDelay; + if (previousCondition > -1 && Item.Condition < previousCondition) + { + // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. + Abandon = true; + } + else + { + // If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here. + previousCondition = Item.Condition; + } + } + private void FindRepairTool() { foreach (Repairable repairable in Item.Repairables) @@ -303,7 +314,6 @@ namespace Barotrauma base.Reset(); goToObjective = null; refuelObjective = null; - previousCondition = -1; repairTool = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3df33e367..b75a3152e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -320,10 +320,10 @@ namespace Barotrauma foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { if (treatmentSuitability.Value <= cprSuitability) { continue; } - if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) + if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab)) { - if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; } - suitableItemIdentifiers.Add(treatmentSuitability.Key); + if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; } + suitableItemIdentifiers.Add(itemPrefab.Identifier); //only list the first 4 items if (itemNameList.Count < 4) { @@ -482,18 +482,6 @@ namespace Barotrauma public static IEnumerable GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs); - public static IEnumerable GetTreatableAfflictions(Character character) - { - var allAfflictions = character.CharacterHealth.GetAllAfflictions(); - foreach (Affliction affliction in allAfflictions) - { - if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } - if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } - if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } - yield return affliction; - } - } - public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index e9cd9e4bb..50f03a240 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -26,7 +26,7 @@ namespace Barotrauma // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. // On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled. - return character == target || manager.HasOrder() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; + return character == target || manager.HasOrder() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold; } } @@ -67,15 +67,34 @@ namespace Barotrauma float vitality = 100; vitality -= character.Bleeding * 2; vitality += Math.Min(character.Oxygen, 0); - vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); - foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character)) + foreach (Affliction affliction in GetTreatableAfflictions(character)) { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; + if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + vitality -= affliction.Strength; + } + else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) + { + vitality -= affliction.Strength; + } } return Math.Clamp(vitality, 0, 100); } + public static IEnumerable GetTreatableAfflictions(Character character) + { + var allAfflictions = character.CharacterHealth.GetAllAfflictions(); + foreach (Affliction affliction in allAfflictions) + { + if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } + if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } + yield return affliction; + } + } + protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index c96c97b8c..14fc0d495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,6 +12,9 @@ namespace Barotrauma private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; + public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 587d61ce9..34092a282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -27,6 +27,15 @@ namespace Barotrauma turrets.Add(turret); // Set false, because we manage the turrets in the Update method. turret.AutoOperate = false; + // Set to full condition, because items don't work when they are broken. + turret.Item.Condition = turret.Item.MaxCondition; + foreach (MapEntity linkedEntity in turret.Item.linkedTo) + { + if (linkedEntity is Item linkedItem) + { + linkedItem.Condition = linkedItem.MaxCondition; + } + } } } LoadAllTurrets(); @@ -264,27 +273,52 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); - bool someoneNearby = false; + bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; - foreach (Submarine submarine in Submarine.Loaded) +#if SERVER + foreach (var client in GameMain.Server.ConnectedClients) { - if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, Submarine.WorldPosition) < minDist * minDist) + var spectatePos = client.SpectatePos; + if (spectatePos.HasValue) { - someoneNearby = true; - break; + if (IsCloseEnough(spectatePos.Value, minDist)) + { + isSomeoneNearby = true; + break; + } } } - foreach (Character c in Character.CharacterList) +#else + if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist)) { - if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } - if (Vector2.DistanceSquared(c.WorldPosition, Submarine.WorldPosition) < minDist * minDist) + isSomeoneNearby = true; + } +#endif + if (!isSomeoneNearby) + { + foreach (Submarine submarine in Submarine.Loaded) { - someoneNearby = true; - break; + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (IsCloseEnough(submarine.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } } } - if (!someoneNearby) { return; } + if (!isSomeoneNearby) + { + foreach (Character c in Character.CharacterList) + { + if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; } + if (IsCloseEnough(c.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } + } + } + if (!isSomeoneNearby) { return; } OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { @@ -292,6 +326,7 @@ namespace Barotrauma UpdateReinforcements(deltaTime); } } + private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist; private void SpawnInitialCells() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 140aa7c04..1606de078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -541,11 +541,11 @@ namespace Barotrauma float wobbleStrength = 0.0f; if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType); } if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType); } if (wobbleStrength <= 0.1f) { return 0.0f; } wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index cec3fcc4a..37111fefd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -150,8 +150,10 @@ namespace Barotrauma private readonly float movementLerp; - private float cprAnimTimer; - private float cprPump; + private float cprAnimTimer,cprPump; + + private float fallingProneAnimTimer; + const float FallingProneAnimDuration = 1.0f; private bool swimming; //time until the character can switch from walking to swimming or vice versa @@ -268,7 +270,8 @@ namespace Barotrauma if (deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; - UpdateDying(deltaTime); + //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration + UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration); } } else @@ -278,6 +281,11 @@ namespace Barotrauma if (!character.CanMove) { + if (fallingProneAnimTimer < FallingProneAnimDuration) + { + fallingProneAnimTimer += deltaTime; + UpdateFallingProne(1.0f); + } levitatingCollider = false; Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) @@ -291,18 +299,20 @@ namespace Barotrauma } return; } + fallingProneAnimTimer = 0.0f; //re-enable collider if (!Collider.Enabled) { var lowestLimb = FindLowestLimb(); - + Collider.SetTransform(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); + Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity; Collider.Enabled = true; } @@ -444,12 +454,13 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = false; + IsHanging = IsHanging && character.IsRagdolled; } void UpdateStanding() { - if (CurrentGroundedParams == null) { return; } + var currentGroundedParams = CurrentGroundedParams; + if (currentGroundedParams == null) { return; } Vector2 handPos; Limb leftFoot = GetLimb(LimbType.LeftFoot); @@ -472,7 +483,7 @@ namespace Barotrauma walkCycleMultiplier *= 1.5f; } - float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale; + float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale; Vector2 colliderPos = GetColliderBottom(); if (Math.Abs(TargetMovement.X) > 1.0f) @@ -573,7 +584,7 @@ namespace Barotrauma } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; @@ -588,7 +599,7 @@ namespace Barotrauma if (!head.Disabled) { - y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; + y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier; if (HeadPosition.HasValue) { y += HeadPosition.Value; } if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = @@ -605,18 +616,18 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); } if (!head.Disabled) { - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque); } else { @@ -655,16 +666,16 @@ namespace Barotrauma if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged - float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); + float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); - if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + if (currentGroundedParams.FootLiftHorizontalFactor > 0) { // Calculate the foot y dynamically based on the foot position relative to the waist, // so that the foot aims higher when it's behind the waist and lower when it's in the front. float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; - float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); - float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor); float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); footPos.Y *= xFactor; } @@ -688,19 +699,19 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = colliderPos + footPos; - MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); + MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } //calculate the positions of hands handPos = torso.SimPosition; - handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X; + handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X; - float lowerY = CurrentGroundedParams.HandClampY; + float lowerY = currentGroundedParams.HandClampY; - handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y)); + handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y)); Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y); @@ -708,13 +719,13 @@ namespace Barotrauma { HandIK(rightHand, torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } if (leftHand != null && !leftHand.Disabled) { HandIK(leftHand, torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } } else @@ -745,8 +756,8 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; - float footMoveForce = CurrentGroundedParams.FootMoveStrength; - float legBendTorque = CurrentGroundedParams.LegBendTorque; + float footMoveForce = currentGroundedParams.FootMoveStrength; + float legBendTorque = currentGroundedParams.LegBendTorque; if (Crouching) { // Keeps the pose @@ -754,7 +765,7 @@ namespace Barotrauma footMoveForce *= 2; } MoveLimb(foot, footPos, footMoveForce); - FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } @@ -770,7 +781,7 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength); } //get the elbow to a neutral rotation @@ -781,14 +792,14 @@ namespace Barotrauma if (elbow != null) { float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); - forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength); } } // Try to keep the wrist straight LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type); if (wrist != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength); + hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength); } } } @@ -1292,10 +1303,9 @@ namespace Barotrauma } } - void UpdateDying(float deltaTime) + void UpdateFallingProne(float strength) { - //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration - float strength = 1.0f - deathAnimTimer / deathAnimDuration; + if (strength <= 0.0f) { return; } Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); @@ -1319,6 +1329,19 @@ namespace Barotrauma } if (torso == null) { return; } + + //make the torso tip over + //otherwise it tends to just drop straight down, pinning the characters legs in a weird pose + if (!InWater) + { + //prefer tipping over in the same direction the torso is rotating + //or moving + //or lastly, in the direction it's facing if it's not moving/rotating + float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f); + float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength; + torso.body.ApplyTorque(torque * torso.body.Mass); + } + //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split for (int i = 0; i < 2; i++) { @@ -1503,12 +1526,12 @@ namespace Barotrauma Limb rightHand = GetLimb(LimbType.RightHand); Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb; + if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb; + if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } if (!target.AllowInput) { @@ -1644,18 +1667,24 @@ namespace Barotrauma pullLimb.PullJointEnabled = true; if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { - Vector2 pullLimbAnchor = targetLimb.SimPosition; pullLimb.PullJointMaxForce = 5000.0f; if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) { targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); } - - Vector2 shoulderPos = rightShoulder.WorldAnchorA; - Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; - if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; } - targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); + Vector2 shoulderPos = rightShoulder.WorldAnchorA; + float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos); + Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist; + if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; } + if (!InWater) + { + //lerp the arm downwards when not swimming + dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f); + } + + Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2); + targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength); targetForce = 200.0f; if (target.Submarine != character.Submarine) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 1d0f78157..01bf6260b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1212,7 +1212,9 @@ namespace Barotrauma RefreshFloorY(ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { - float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY()); + (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); + float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits); + float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); @@ -1222,13 +1224,21 @@ namespace Barotrauma { //set floorY to the position of the floor in the hull below the character var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false); - if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + if (lowerHull != null) + { + floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + } } } float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; - if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f) + if (Collider.SimPosition.Y < waterSurfaceY) { - inWater = true; + //too deep to stand up, or not enough room to stand up + if (waterSurfaceY - floorY > standHeight * 0.8f || + ceilingY - floorY < standHeight * 0.8f) + { + inWater = true; + } } } } @@ -1663,22 +1673,34 @@ namespace Barotrauma } } + /// + /// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in) + /// public float GetSurfaceY() + { + return GetWaterSurfaceAndCeilingY().WaterSurfaceY; + } + + /// + /// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in). + /// + private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY() { //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside if (currentHull == null || character.CurrentHull == null) { - return float.PositiveInfinity; + return (float.PositiveInfinity, float.PositiveInfinity); } - - float surfacePos = currentHull.Surface; + + float surfaceY = currentHull.Surface; + float ceilingY = currentHull.Rect.Y; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) - { - GetSurfacePos(currentHull, ref surfacePos); - void GetSurfacePos(Hull hull, ref float prevSurfacePos) + { + GetSurfacePos(currentHull, ref surfaceY, ref ceilingY); + void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos) { if (prevSurfacePos > surfaceThreshold) { return; } foreach (Gap gap in hull.ConnectedGaps) @@ -1689,6 +1711,7 @@ namespace Barotrauma //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { + ceilingPos += 100000.0f; prevSurfacePos += 100000.0f; return; } @@ -1697,15 +1720,16 @@ namespace Barotrauma { if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { - prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); - GetSurfacePos(otherHull, ref prevSurfacePos); + prevSurfacePos = Math.Max(surfaceY, otherHull.Surface); + ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y); + GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos); break; } } } } } - return surfacePos; + return (surfaceY, ceilingY); } public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index d60b3c926..3b4bc3f15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -343,9 +343,10 @@ namespace Barotrauma return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime; } - public float GetItemDamage(float deltaTime) + public float GetItemDamage(float deltaTime, float multiplier = 1) { - return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime; + float dmg = ItemDamage * multiplier; + return (Duration == 0.0f) ? dmg : dmg * deltaTime; } public float GetTotalDamage(bool includeStructureDamage = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 61fa1af24..17b174c8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -31,6 +31,9 @@ namespace Barotrauma { public readonly static List CharacterList = new List(); + public const float MaxHighlightDistance = 150.0f; + public const float MaxDragDistance = 200.0f; + partial void UpdateLimbLightSource(Limb limb); private bool enabled = true; @@ -176,6 +179,13 @@ namespace Barotrauma } } + private Identifier? faction; + public Identifier Faction + { + get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; } + set { faction = value; } + } + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -497,7 +507,7 @@ namespace Barotrauma LocalizedString displayName = Params.DisplayName; if (displayName.IsNullOrWhiteSpace()) { - if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) + if (Params.SpeciesTranslationOverride.IsEmpty) { displayName = TextManager.Get($"Character.{SpeciesName}"); } @@ -529,8 +539,13 @@ namespace Barotrauma } set { + bool wasHidden = HideFace; hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); - if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true); + bool isHidden = HideFace; + if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden) + { + info.CheckDisguiseStatus(true); + } } } @@ -760,7 +775,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); } } @@ -822,9 +837,15 @@ namespace Barotrauma public float HealthPercentage => CharacterHealth.HealthPercentage; public float MaxVitality => CharacterHealth.MaxVitality; public float MaxHealth => MaxVitality; + + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasFullHealth => CharacterHealth.WasInFullHealth; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; + public float PoisonVulnerability => Params.Health.PoisonVulnerability; public float Bloodloss { @@ -838,7 +859,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength("bleeding", true); } + get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -1043,6 +1064,8 @@ namespace Barotrauma public bool InWater => AnimController is AnimController { InWater: true }; + public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold; + public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; @@ -1099,6 +1122,12 @@ namespace Barotrauma public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; + public float AITurretPriority + { + get => Params.AITurretPriority; + private set => Params.AITurretPriority = value; + } + public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); public OnDeathHandler OnDeath; @@ -1691,7 +1720,7 @@ namespace Barotrauma if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) { skillLevel += skillValue; - break; + break; } } @@ -1700,9 +1729,7 @@ namespace Barotrauma } skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); - - - return skillLevel; + return Math.Max(skillLevel, 0); } // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner @@ -1900,7 +1927,7 @@ namespace Barotrauma { if (limb != null) { - sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage")); + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType)); } return Math.Clamp(sum, 0, 1f); } @@ -2201,7 +2228,9 @@ namespace Barotrauma if (SelectedCharacter != null) { - if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected) + if (!SelectedCharacter.CanBeSelected || + (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance && + SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance))) { DeselectCharacter(); } @@ -2494,8 +2523,12 @@ namespace Barotrauma if (!skipDistanceCheck) { - maxDist = ConvertUnits.ToSimUnits(maxDist); - if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } + maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent()); + if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist && + Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist) + { + return false; + } } return checkVisibility ? CanSeeCharacter(c) : true; @@ -2851,6 +2884,23 @@ namespace Barotrauma } else { +#if CLIENT + if (Controlled == this) + { + HealingCooldown.PutOnCooldown(); + } +#elif SERVER + if (GameMain.Server?.ConnectedClients is { } clients) + { + foreach (Client c in clients) + { + if (c.Character != this) { continue; } + + HealingCooldown.SetCooldown(c); + break; + } + } +#endif SelectCharacter(FocusedCharacter); #if CLIENT if (Controlled == this) @@ -3070,8 +3120,7 @@ namespace Barotrauma if (NeedsAir) { //implode if not protected from pressure, and either outside or in a high-pressure hull - if (!IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { if (CharacterHealth.PressureKillDelay <= 0.0f) { @@ -3098,15 +3147,17 @@ namespace Barotrauma PressureTimer = 0.0f; } } - else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && - PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && - WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure)) + else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure) { - //implode if below crush depth, and either outside or in a high-pressure hull - if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f; + if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth) { - Implode(); - if (IsDead) { return; } + //implode if below crush depth, and either outside or in a high-pressure hull + if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + { + Implode(); + if (IsDead) { return; } + } } } @@ -3140,56 +3191,57 @@ namespace Barotrauma UpdateAIChatMessages(deltaTime); - //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! - bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; - bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f; - bool wasRagdolled = false; - bool selfRagdolled = false; - - if (IsForceRagdolled) + if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true) { - IsRagdolled = IsForceRagdolled; - } - else if (this != Controlled) - { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); - } - //Keep us ragdolled if we were forced or we're too speedy to unragdoll - else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll)) - { - if (ragdollingLockTimer > 0.0f) + bool wasRagdolled = IsRagdolled; + if (IsForceRagdolled) { - SetInput(InputType.Ragdoll, false, true); - ragdollingLockTimer -= deltaTime; + IsRagdolled = IsForceRagdolled; + } + else if (this != Controlled) + { + wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); } else { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; } + bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); + bool bodyMovingTooFast(PhysicsBody body) + { + return + body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || + //falling down counts as going too fast + (!InWater && body.LinearVelocity.Y < -5.0f); + } + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else if (!tooFastToUnragdoll) + { + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + } + if (IsRagdolled) + { + SetInput(InputType.Ragdoll, false, true); + } } - } - - if (!wasRagdolled && IsRagdolled) - { - if (selfRagdolled) + if (!wasRagdolled && IsRagdolled) { - CheckTalents(AbilityEffectType.OnSelfRagdoll); + CheckTalents(AbilityEffectType.OnRagdoll); } - // currently does not work when you are stunned, like it should - CheckTalents(AbilityEffectType.OnRagdoll); } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); - //ragdoll button if (IsRagdolled || !CanMove) { if (AnimController is HumanoidAnimController humanAnimController) { humanAnimController.Crouching = false; } + if (IsRagdolled) { AnimController.IgnorePlatforms = true; } AnimController.ResetPullJoints(); SelectedItem = SelectedSecondaryItem = null; return; @@ -3367,6 +3419,20 @@ namespace Barotrauma return distSqr; } + public float GetDistanceToClosestLimb(Vector2 simPos) + { + float closestDist = float.MaxValue; + foreach (Limb limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + float dist = Vector2.Distance(simPos, limb.SimPosition); + dist -= limb.body.GetMaxExtent(); + closestDist = Math.Min(closestDist, dist); + if (closestDist <= 0.0f) { return 0.0f; } + } + return closestDist; + } + private float despawnTimer; private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) { @@ -3758,9 +3824,10 @@ namespace Barotrauma message.SendDelay -= deltaTime; if (message.SendDelay > 0.0f) { continue; } + bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio); if (message.MessageType == null) { - message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default; + message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; } #if CLIENT if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) @@ -3770,6 +3837,11 @@ namespace Barotrauma { GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this); } + if (canUseRadio) + { + Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item); + radio.TransmitSignal(s, sentFromChat: true); + } } #endif #if SERVER @@ -4005,17 +4077,7 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } - CreatureMetrics.Instance.Killed.Add(target.SpeciesName); - AddEncounter(target); - } - - public void AddEncounter(Character other) - { - if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } - CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); - CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); + CreatureMetrics.RecordKill(target.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -4082,17 +4144,23 @@ namespace Barotrauma OnAttackedProjSpecific(attacker, attackResult, stun); if (!wasDead) { - TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality); + TryAdjustAttackerSkill(attacker, attackResult); } - }; + } if (attackResult.Damage > 0) { LastDamage = attackResult; - if (attacker != null) + if (attacker != null && attacker != this && !attacker.Removed) { AddAttacker(attacker, attackResult.Damage); - AddEncounter(attacker); - attacker.AddEncounter(this); + if (IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(attacker.SpeciesName); + } + if (attacker.IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(SpeciesName); + } } ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); @@ -4108,26 +4176,85 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun); - public void TryAdjustAttackerSkill(Character attacker, float healthChange) + public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult) { if (attacker == null) { return; } - + if (!attacker.IsOnPlayerTeam) { return; } bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID; - if (isEnemy) + if (!isEnemy) { return; } + float weaponDamage = 0; + float medicalDamage = 0; + foreach (var affliction in attackResult.Afflictions) { - if (healthChange < 0.0f) + if (affliction.Prefab.IsBuff) { continue; } + if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { - float attackerSkillLevel = attacker.GetSkillLevel("weapons"); - attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(), - -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); + if (!Params.Health.PoisonImmunity) + { + float relativeVitality = MaxVitality / 100f; + // Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied. + float dmg = affliction.Strength; + if (relativeVitality > 0) + { + dmg /= relativeVitality; + } + if (PoisonVulnerability > 0) + { + dmg /= PoisonVulnerability; + } + float strength = MaxVitality; + if (Params.AI != null) + { + strength = Params.AI.CombatStrength; + } + // Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x. + float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength)); + dmg *= vitalityFactor; + medicalDamage += dmg * affliction.Prefab.MedicalSkillGain; + } } + else + { + medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain; + } + weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain; } - else if (healthChange > 0.0f) + if (medicalDamage > 0) { - float attackerSkillLevel = attacker.GetSkillLevel("medical"); - attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(), - healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f)); + IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage); } + if (weaponDamage > 0) + { + IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage); + } + + void IncreaseSkillLevel(Identifier skill, float damage) + { + float attackerSkillLevel = attacker.GetSkillLevel(skill); + // The formula is too generous on low skill levels, hence the minimum divider. + float minSkillDivider = 15f; + attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider)); + } + } + + public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null) + { + if (healer == null) { return; } + bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID; + if (isEnemy) { return; } + float medicalGain = healthChange; + if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines)) + { + medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain; + } + if (medicalGain <= 0) { return; } + Identifier skill = new Identifier("medical"); + float attackerSkillLevel = healer.GetSkillLevel(skill); + // The formula is too generous on low skill levels, hence the minimum divider. + float minSkillDivider = 15f; + healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider)); } /// @@ -4142,7 +4269,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4169,7 +4296,7 @@ namespace Barotrauma float eatingRegen = Params.Health.HealthRegenerationWhenEating; if (eatingRegen > 0) { - CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime); + CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime); } } if (statusEffects.TryGetValue(actionType, out var statusEffectList)) @@ -4541,6 +4668,7 @@ namespace Barotrauma Submarine = null; AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false); AnimController.FindHull(worldPos, setSubmarine: true); + CurrentHull = AnimController.CurrentHull; if (AIController is HumanAIController humanAI) { humanAI.PathSteering?.ResetPath(); @@ -4787,34 +4915,36 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) { - Vector2 targetPos = target.SimPosition; + Vector2 targetPos = to.SimPosition; if (worldPos.HasValue) { Vector2 wp = worldPos.Value; - if (target.Submarine != null) + if (to.Submarine != null) { - wp -= target.Submarine.Position; + wp -= to.Submarine.Position; } targetPos = ConvertUnits.ToSimUnits(wp); } - if (Submarine == null && target.Submarine != null) + if (from.Submarine == null && to.Submarine != null) { // outside and targeting inside - targetPos += target.Submarine.SimPosition; + targetPos += to.Submarine.SimPosition; } - else if (Submarine != null && target.Submarine == null) + else if (from.Submarine != null && to.Submarine == null) { // inside and targeting outside - targetPos -= Submarine.SimPosition; + targetPos -= from.Submarine.SimPosition; } - else if (Submarine != target.Submarine) + else if (from.Submarine != to.Submarine) { - if (Submarine != null && target.Submarine != null) + if (from.Submarine != null && to.Submarine != null) { // both inside, but in different subs - Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition; + Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; targetPos -= diff; } } @@ -4836,13 +4966,14 @@ namespace Barotrauma public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; - public bool IsProtectedFromPressure() - { - return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - } + /// + /// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit). + /// + public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - // Talent logic begins here. Should be encapsulated to its own controller soon + public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); + #region Talents private readonly List characterTalents = new List(); public void LoadTalents() @@ -4916,6 +5047,49 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) + { + return false; + } + } + } + } + return true; + } + + public bool HasTalents() + { + return characterTalents.Any(); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityObject); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, null); + } + } + + partial void OnTalentGiven(TalentPrefab talentPrefab); + + #endregion + private readonly HashSet sameRoomHulls = new(); /// @@ -4942,24 +5116,6 @@ namespace Barotrauma return sameRoomHulls.Contains(character.CurrentHull); } - public bool HasUnlockedAllTalents() - { - if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (!talentOption.HasMaxTalents(info.UnlockedTalents)) - { - return false; - } - } - } - } - return true; - } - public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -4969,27 +5125,6 @@ namespace Barotrauma return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); } - public bool HasTalents() - { - return characterTalents.Any(); - } - - public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, abilityObject); - } - } - - public void CheckTalents(AbilityEffectType abilityEffectType) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, null); - } - } - public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); @@ -5053,7 +5188,6 @@ namespace Barotrauma #endif partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -5208,7 +5342,24 @@ namespace Barotrauma public bool IsFriendly(Character other) => IsFriendly(this, other); - public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); + public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); + + public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) + { + if (myTeam == otherTeam) { return true; } + return myTeam switch + { + // NPCs are friendly to the same team and the friendly NPCs + CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, + // Friendly NPCs are friendly to both player teams + CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, + _ => true + }; + } + + public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); + public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID); + public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam); public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index cb1b33763..d38730699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; -using static Barotrauma.CharacterInfo; namespace Barotrauma { @@ -19,6 +17,8 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } + public CharacterPrefab ParentPrefab { get; set; } + public void InheritFrom(CharacterPrefab parent) { ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); @@ -38,7 +38,7 @@ namespace Barotrauma } } - private XElement originalElement; + private readonly XElement originalElement; public ContentXElement ConfigElement { get; private set; } public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } @@ -49,10 +49,6 @@ namespace Barotrauma public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); - /// - /// Searches for a character config file from all currently selected content packages, - /// or from a specific package if the contentPackage parameter is given. - /// public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { if (!Prefabs.ContainsKey(speciesName)) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 33368e40a..073ff8e37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -19,6 +19,10 @@ namespace Barotrauma private float fluctuationTimer; + private AfflictionPrefab.Effect activeEffect; + private float prevActiveEffectStrength; + protected bool activeEffectDirty = true; + protected float _strength; [Serialize(0f, IsPropertySaveable.Yes), Editable] @@ -46,6 +50,7 @@ namespace Barotrauma Duration = Prefab.Duration; } _strength = newValue; + activeEffectDirty = true; } } @@ -147,7 +152,16 @@ namespace Barotrauma MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; } - public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); + public AfflictionPrefab.Effect GetActiveEffect() + { + if (activeEffectDirty) + { + activeEffect = Prefab.GetActiveEffect(_strength); + prevActiveEffectStrength = _strength; + activeEffectDirty = false; + } + return activeEffect; + } public float GetVitalityDecrease(CharacterHealth characterHealth) { @@ -158,7 +172,7 @@ namespace Barotrauma { if (strength < Prefab.ActivationThreshold) { return 0.0f; } strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength); - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } @@ -349,7 +363,7 @@ namespace Barotrauma public float GetStatValue(StatTypes statType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; } if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) { @@ -363,7 +377,7 @@ namespace Barotrauma public bool HasFlag(AbilityFlags flagType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; } return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } @@ -415,6 +429,7 @@ namespace Barotrauma } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); + activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { @@ -442,7 +457,10 @@ namespace Barotrauma var currentEffect = GetActiveEffect(); if (currentEffect != null) { - currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb)); + foreach (var statusEffect in currentEffect.StatusEffects) + { + ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb); + } } } @@ -481,6 +499,7 @@ namespace Barotrauma { _nonClampedStrength = strength; _strength = _nonClampedStrength; + activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength); } public bool ShouldShowIcon(Character afflictedCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 9e9384a94..dca86c889 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -43,6 +43,7 @@ namespace Barotrauma DeactivateHusk(); highestStrength = 0; } + activeEffectDirty = true; } } private float highestStrength; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 8ccef8d3b..652cc60f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -319,6 +319,7 @@ namespace Barotrauma { public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; + public readonly float MinStrength, MaxStrength; public PeriodicEffect(ContentXElement element, string parentDebugName) { @@ -333,23 +334,38 @@ namespace Barotrauma } else { - MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f); - MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval); + MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f); + MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval); + MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f); + MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength); } } } + public static readonly Identifier DamageType = "damage".ToIdentifier(); + public static readonly Identifier BurnType = "burn".ToIdentifier(); + public static readonly Identifier BleedingType = "bleeding".ToIdentifier(); + public static readonly Identifier ParalysisType = "paralysis".ToIdentifier(); + public static readonly Identifier PoisonType = "poison".ToIdentifier(); + public static readonly Identifier StunType = "stun".ToIdentifier(); + public static readonly Identifier EMPType = "emp".ToIdentifier(); + public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); + public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); + public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); + public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier(); + 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"]; + public static AfflictionPrefab Bleeding => Prefabs[BleedingType]; + public static AfflictionPrefab Burn => Prefabs[BurnType]; public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; - public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public override void Dispose() { } @@ -413,8 +429,8 @@ namespace Barotrauma //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; - public float BurnOverlayAlpha; - public float DamageOverlayAlpha; + public readonly float BurnOverlayAlpha; + public readonly float DamageOverlayAlpha; //steam achievement given when the affliction is removed from the controlled character public readonly Identifier AchievementOnRemoved; @@ -425,6 +441,20 @@ namespace Barotrauma public readonly Sprite AfflictionOverlay; public readonly bool AfflictionOverlayAlphaIsLinear; + public readonly bool DamageParticles; + + /// + /// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target. + /// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile. + /// If the affliction is of type buff, the skill is increased only when the target is friendly. + /// + public readonly float MedicalSkillGain; + /// + /// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target. + /// The skill is increased only when the target is hostile. + /// + public readonly float WeaponsSkillGain; + private readonly List effects = new List(); private readonly List periodicEffects = new List(); @@ -519,8 +549,14 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); - CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); - SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); + CauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", ""))) + .Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", ""))) + .Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray(nameof(IconColors), null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); @@ -530,6 +566,10 @@ namespace Barotrauma ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); + DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true); + WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f); + MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f); + List descriptions = new List(); foreach (var subElement in element.Elements()) { @@ -604,6 +644,18 @@ namespace Barotrauma break; } } + for (int i = 0; i < effects.Count; i++) + { + for (int j = i + 1; j < effects.Count; j++) + { + var a = effects[i]; + var b = effects[j]; + if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength) + { + DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work."); + } + } + } } public void ClearEffects() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index c5ee4f6a4..46b096bb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -230,6 +230,11 @@ namespace Barotrauma public float StunTimer { get; private set; } + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasInFullHealth { get; private set; } + public Affliction PressureAffliction { get { return pressureAffliction; } @@ -334,12 +339,12 @@ namespace Barotrauma return null; } - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + public T GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction { return GetAffliction(identifier, allowLimbAfflictions) as T; } - public Affliction GetAffliction(string identifier, Limb limb) + public Affliction GetAffliction(Identifier identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { @@ -375,7 +380,7 @@ namespace Barotrauma /// The limb the affliction is attached to /// Does the affliction have to be attached to only the specific limb. /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. - public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) + public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific) { if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } @@ -396,7 +401,7 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -478,16 +483,19 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } - + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(afflictions.Keys); - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + foreach (var affliction in afflictions) + { + if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) + { + matchingAfflictions.Add(affliction.Key); + } + } + ReduceMatchingAfflictions(amount, treatmentAction); } @@ -504,18 +512,21 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } - + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; + foreach (var affliction in afflictions) + { + if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) && + affliction.Value == targetLimbHealth) + { + matchingAfflictions.Add(affliction.Key); + } + } ReduceMatchingAfflictions(amount, treatmentAction); } @@ -617,7 +628,7 @@ namespace Barotrauma KillIfOutOfVitality(); } - public float GetLimbDamage(Limb limb, string afflictionType = null) + public float GetLimbDamage(Limb limb, Identifier afflictionType) { float damageStrength; if (limb.IsSevered) @@ -630,16 +641,16 @@ namespace Barotrauma // Therefore with e.g. 80 health, the max damage per limb would be 40. // Having at least 40 damage on both legs would cause maximum limping. float max = MaxVitality / 2; - if (string.IsNullOrEmpty(afflictionType)) + if (afflictionType.IsEmpty) { - float damage = GetAfflictionStrength("damage", limb, true); - float bleeding = GetAfflictionStrength("bleeding", limb, true); - float burn = GetAfflictionStrength("burn", limb, true); + float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true); + float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true); + float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true); damageStrength = Math.Min(damage + bleeding + burn, max); } else { - damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max); + damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max); } return damageStrength / max; } @@ -696,15 +707,16 @@ namespace Barotrauma if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } } - if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } - if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } + if (Character.Params.Health.PoisonImmunity && + (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } Affliction existingAffliction = null; @@ -742,7 +754,8 @@ namespace Barotrauma Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); - + MedicalClinic.OnAfflictionCountChanged(Character); + Character.HealthUpdateInterval = 0.0f; CalculateVitality(); @@ -766,6 +779,8 @@ namespace Barotrauma public void Update(float deltaTime) { + WasInFullHealth = vitality >= MaxVitality; + UpdateOxygen(deltaTime); StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; @@ -813,10 +828,16 @@ namespace Barotrauma } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } + foreach (var affliction in afflictionsToRemove) { afflictions.Remove(affliction); - } + } + + if (afflictionsToRemove.Count is not 0) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); @@ -870,6 +891,11 @@ namespace Barotrauma } } + /// + /// 0-1. + /// + public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab); + private void UpdateOxygen(float deltaTime) { if (!Character.NeedsOxygen) @@ -978,6 +1004,8 @@ namespace Barotrauma UpdateLimbAfflictionOverlays(); UpdateSkinTint(); Character.Kill(type, affliction); + + WasInFullHealth = false; #if CLIENT DisplayVitalityDelay = 0.0f; DisplayedVitality = Vitality; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index ab58d8faa..a58651d84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -490,13 +490,9 @@ namespace Barotrauma public int RefJointIndex => Params.RefJoint; - private List wearingItems; - public List WearingItems - { - get { return wearingItems; } - } + public readonly List WearingItems = new List(); - public List OtherWearables { get; private set; } = new List(); + public readonly List OtherWearables = new List(); public bool PullJointEnabled { @@ -640,7 +636,6 @@ namespace Barotrauma this.ragdoll = ragdoll; this.character = character; this.Params = limbParams; - wearingItems = new List(); dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; @@ -772,7 +767,7 @@ namespace Barotrauma tempModifiers.Add(damageModifier); } } - foreach (WearableSprite wearable in wearingItems) + foreach (WearableSprite wearable in WearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { @@ -791,10 +786,15 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; - if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0) + if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { finalDamageModifier *= character.EmpVulnerability; } + if (!character.Params.Health.PoisonImmunity && + (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) + { + finalDamageModifier *= character.PoisonVulnerability; + } foreach (DamageModifier damageModifier in tempModifiers) { float damageModifierValue = damageModifier.DamageMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 7dadbaf86..ba88edee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -21,7 +21,7 @@ namespace Barotrauma public Identifier SpeciesName { get; private set; } [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] - public string SpeciesTranslationOverride { get; private set; } + public Identifier SpeciesTranslationOverride { get; private set; } [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } @@ -113,6 +113,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool DrawLast { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] + public float AITurretPriority { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] + public float AISlowTurretPriority { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -479,7 +485,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)] public float CrushDepth { get; set; } // Make editable? @@ -504,6 +510,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } + [Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)] + public float PoisonVulnerability { get; set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float EmpVulnerability { get; set; } @@ -512,7 +521,20 @@ namespace Barotrauma // TODO: limbhealths, sprite? - public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) + { + //backwards compatibility + if (CrushDepth < 0) + { + //invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level) + float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000; + DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+ + "Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+ + "but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+ + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}."); + CrushDepth = newCrushDepth; + } + } } public class InventoryParams : SubParam diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 0ac951fd6..c07128f0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -28,7 +28,6 @@ namespace Barotrauma.Abilities conditionals.Add(new PropertyConditional(attribute)); } } - break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 993c19b94..e4580fadd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -37,19 +37,7 @@ namespace Barotrauma.Abilities if (itemPrefab != null) { - if (category != MapEntityCategory.None) - { - if (!itemPrefab.Category.HasFlag(category)) { return false; } - } - - if (identifiers.Any()) - { - if (!identifiers.Any(t => itemPrefab.Identifier == t)) - { - return false; - } - } - return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + return MatchesItem(itemPrefab); } else { @@ -57,5 +45,22 @@ namespace Barotrauma.Abilities return false; } } + + public bool MatchesItem(ItemPrefab itemPrefab) + { + if (category != MapEntityCategory.None) + { + if (!itemPrefab.Category.HasFlag(category)) { return false; } + } + + if (identifiers.Any()) + { + if (!identifiers.Any(t => itemPrefab.Identifier == t)) + { + return false; + } + } + return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 87446e016..a71667245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -8,19 +8,19 @@ namespace Barotrauma.Abilities class AbilityConditionMission : AbilityConditionData { private readonly ImmutableHashSet missionType; - private readonly ImmutableHashSet factions; + private readonly bool isAffiliated; public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; HashSet missionTypes = new HashSet(); - factions = conditionElement.GetAttributeIdentifierImmutableHashSet("faction", ImmutableHashSet.Empty); + isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); foreach (string missionTypeString in missionTypeStrings) { if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) { - if (factions.IsEmpty) + if (!isAffiliated) { DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); } @@ -37,23 +37,28 @@ namespace Barotrauma.Abilities { if (abilityObject is IAbilityMission { Mission: { } mission }) { - if (factions.Any()) - { - if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + if (!isAffiliated) { return CheckMissionType(); } - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + { + if (amount <= 0) { continue; } + if (GetMatchingFaction(factionIdentifier) is { } faction && + Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { - if (amount <= 0) { continue; } - if (factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier) is Faction faction && - Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) - { - return true; - } + return CheckMissionType(); } - return false; } - return missionType.Contains(mission.Prefab.Type); + return false; + + Faction GetMatchingFaction(Identifier factionIdentifier) => + factionIdentifier == "location" + ? mission.OriginLocation?.Faction + : factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier); + + bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type); } LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs index 78d7f501a..60d7abd61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -1,19 +1,35 @@ #nullable enable +using Barotrauma.Extensions; using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Abilities { internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility { - public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + private readonly AbilityConditionItem? condition; + + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem)); + if (conditionElement != null) + { + condition = new AbilityConditionItem(CharacterTalent, conditionElement); + } + } protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } - int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); - ingredients.Items.RemoveAt(randomIndex); + List applicableIngredients = condition == null ? + ingredients.Items.ToList() : + ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList(); + if (applicableIngredients.None()) { return; } + + ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index a53d13328..b4b26579b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -42,7 +42,7 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; public Md5Hash Hash { get; private set; } - public readonly Option InstallTime; + public readonly Option InstallTime; public ImmutableArray Files { get; private set; } @@ -73,7 +73,7 @@ namespace Barotrauma Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); if (item is null) { return true; } - return item.Value.LatestUpdateTime <= installTime; + return item.Value.LatestUpdateTime <= installTime.ToUtcValue(); } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); @@ -106,10 +106,7 @@ namespace Barotrauma GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); - UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); - InstallTime = installTimeUnix != 0 - ? Option.Some(ToolBox.Epoch.ToDateTime(installTimeUnix)) - : Option.None(); + InstallTime = rootElement.GetAttributeDateTime("installtime"); var fileResults = rootElement.Elements() .Select(e => ContentFile.CreateFromXElement(this, e)) @@ -288,9 +285,7 @@ namespace Barotrauma if (errorCatcher.Errors.Any()) { - yield return ContentPackageManager.LoadProgress.Failure( - ContentPackageManager.LoadProgress.Error - .Reason.ConsoleErrorsThrown); + yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text)); yield break; } yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index e4bb04ce9..88ff41f52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -437,22 +437,19 @@ namespace Barotrauma public readonly record struct LoadProgress(Result Result) { public readonly record struct Error( - Error.Reason ErrorReason, - Option Exception) + Either, Exception> ErrorsOrException) { - public enum Reason { Exception, ConsoleErrorsThrown } - - public Error(Reason reason) : this(reason, Option.None) { } - public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { } + public Error(IEnumerable errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { } + public Error(Exception exception) : this(ErrorsOrException: exception) { } } public static LoadProgress Failure(Exception exception) => new LoadProgress( Result.Failure(new Error(exception))); - public static LoadProgress Failure(Error.Reason reason) + public static LoadProgress Failure(IEnumerable errorMessages) => new LoadProgress( - Result.Failure(new Error(reason))); + Result.Failure(new Error(errorMessages))); public static LoadProgress Progress(float value) => new LoadProgress( diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8f29501bd..dbb30b209 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1443,7 +1443,7 @@ namespace Barotrauma commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); }, () => { @@ -1887,6 +1887,9 @@ namespace Barotrauma commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null)); commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); + commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true)); + commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true)); InitProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 1f5ea9e81..0b8c80af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -49,7 +49,6 @@ namespace Barotrauma OnUseRangedWeapon, OnReduceAffliction, OnAddDamageAffliction, - OnSelfRagdoll, OnRagdoll, OnRoundEnd, OnLootCharacter, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index bdbf2928c..957508205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -8,7 +7,7 @@ using System.Linq; namespace Barotrauma { - class MissionAction : EventAction + partial class MissionAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionIdentifier { get; set; } @@ -106,7 +105,8 @@ namespace Barotrauma IconColor = unlockedMission.Prefab.IconColor }; #else - NotifyMissionUnlock(unlockedMission, unlockLocation); + missionsUnlockedThisRound.Add(unlockedMission); + NotifyMissionUnlock(unlockedMission); #endif } } @@ -186,21 +186,5 @@ namespace Barotrauma { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } - -#if SERVER - private static void NotifyMissionUnlock(Mission mission, Location unlockLocation) - { - foreach (Client client in GameMain.Server.ConnectedClients) - { - IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); - outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); - outmsg.WriteIdentifier(mission.Prefab.Identifier); - outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(unlockLocation) ?? -1); - outmsg.WriteString(mission.Name.Value); - GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); - } - } -#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index 44fdc7144..013b48771 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -69,8 +69,8 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\"."); } - else - { + else if (!location.LocationTypeChangesBlocked) + { location.ChangeType(campaign, locationType); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index d39a5e666..348e856e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { @@ -11,17 +9,20 @@ namespace Barotrauma public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier ItemIdentifier { get; set; } + public string ItemIdentifiers { get; set; } [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (ItemIdentifier.IsEmpty) + private readonly ImmutableHashSet itemIdentifierSplit; + + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (string.IsNullOrEmpty(ItemIdentifiers)) { - ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); + ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty)); } + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet(); } private bool isFinished = false; @@ -62,7 +63,7 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); + (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true); if (item == null) { break; } Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); @@ -70,7 +71,7 @@ namespace Barotrauma } else if (target is Item item) { - if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) + if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier)) { Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 695b9697f..84d8eaa1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -93,7 +93,13 @@ namespace Barotrauma { ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; //backwards compatibility - TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + if (element.GetAttribute("submarinetype") != null) + { + DebugConsole.ThrowError( + $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + } } public override bool IsFinished(ref string goTo) @@ -320,30 +326,24 @@ namespace Barotrauma public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; - List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); - - potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); - + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable); if (moduleFlags != null && moduleFlags.Any()) { - List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList(); + var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains)); if (spawnPoints.Any()) { - potentialSpawnPoints = spawnPoints; + potentialSpawnPoints = spawnPoints.ToList(); } } - if (spawnpointTags != null && spawnpointTags.Any()) { - var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - + var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable)); if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.None()) { if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 89eff4f39..08a5116fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -119,12 +119,12 @@ namespace Barotrauma private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHullsByName(Identifier name) @@ -137,6 +137,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } + private bool IsValidItem(Item it) + { + return !it.HiddenInGame && SubmarineTypeMatches(it.Submarine); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index 3182cfae9..ac2beeebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction TurbineOutputSlider, DeconstructButton, RechargeSpeedSlider, - CPRButton + CPRButton, + CloseButton, + MessageBoxCloseButton } [Serialize(ElementId.None, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 2a07a41fb..a6f01c406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -135,7 +135,9 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); - +#if SERVER + MissionAction.ResetMissionsUnlockedThisRound(); +#endif pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index d89d3e054..58d243d43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -147,10 +147,7 @@ namespace Barotrauma ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPoint == null) - { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { @@ -189,7 +186,12 @@ namespace Barotrauma if (element.Attribute("identifier") != null && element.Attribute("from") != null) { - HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found"); + continue; + } for (int i = 0; i < count; i++) { LoadHuman(humanPrefab, element, submarine); @@ -201,7 +203,7 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found"); continue; } for (int i = 0; i < count; i++) @@ -223,10 +225,7 @@ namespace Barotrauma moduleFlags ?? humanPrefab.GetModuleFlags(), spawnPointTags ?? humanPrefab.GetSpawnPointTags(), element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); bool requiresRescue = element.GetAttributeBool("requirerescue", false); var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); @@ -255,6 +254,13 @@ namespace Barotrauma } #endif } + else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi) + { + var order = OrderPrefab.Prefabs["fightintruders"] + .CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false); + } if (element.GetAttributeBool("requirekill", false)) { @@ -267,10 +273,7 @@ namespace Barotrauma Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); if (element.GetAttributeBool("requirekill", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index c480ceccd..daa064131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -203,8 +203,11 @@ namespace Barotrauma projectileTimer -= deltaTime; if (projectileTimer <= 0.0f) { + float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); + float distanceFactor = Math.Min(dist / 10000.0f, 1.0f); int projectileAmount = Rand.Range(3, 6); - float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)); + //more concentrated shots the further the sub is + float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f); for (int i = 0; i < projectileAmount; i++) { int index = i; @@ -218,13 +221,13 @@ namespace Barotrauma } it.body.SetTransform(it.SimPosition, angle); it.UpdateTransform(); - projectile.Use(); + //faster launch velocity the further the sub is + projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor)); }); } - float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); //the closer the sub is, more likely it is to shoot frequently - float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, dist / 15000.0f); + float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor); if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability) { projectileTimer = Rand.Range(3.0f, 5.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index da43f1289..c0f125362 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -42,6 +42,8 @@ namespace Barotrauma } } + public int TimesAttempted { get; set; } + protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; private readonly CheckDataAction completeCheckDataAction; @@ -49,6 +51,12 @@ namespace Barotrauma public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; + /// + /// The reward that was actually given from completing the mission, taking any talent bonuses into account + /// (some of which may not be possible to determine in advance) + /// + private int? finalReward; + public virtual LocalizedString Name => Prefab.Name; private readonly LocalizedString successMessage; @@ -367,6 +375,8 @@ namespace Barotrauma GiveReward(); } + TimesAttempted++; + EndMissionSpecific(completed); } @@ -374,6 +384,27 @@ namespace Barotrauma protected virtual void EndMissionSpecific(bool completed) { } + /// + /// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly. + /// + public int GetFinalReward(Submarine sub) + { + return finalReward ?? GetReward(sub); + } + + /// + /// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney, + /// and should only be called once when the mission is completed! + /// + private void CalculateFinalReward(Submarine sub) + { + int reward = GetReward(sub); + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + finalReward = (int)(reward * missionMoneyGainMultiplier.Value); + } private void GiveReward() { @@ -417,38 +448,35 @@ namespace Barotrauma info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } - // apply money gains afterwards to prevent them from affecting XP gains - var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); - crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - - int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); - + CalculateFinalReward(Submarine.MainSub); #if SERVER - totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); + finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value); #endif bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (isSingleplayerOrServer && totalReward > 0) + if (isSingleplayerOrServer) { - campaign.Bank.Give(totalReward); - } - - foreach (Character character in crewCharacters) - { - character.Info.MissionsCompletedSinceDeath++; - } - - foreach (KeyValuePair reputationReward in ReputationRewards) - { - if (reputationReward.Key == "location") + if (finalReward > 0) { - OriginLocation.Reputation?.AddReputation(reputationReward.Value); + campaign.Bank.Give(finalReward.Value); } - else + + foreach (Character character in crewCharacters) { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } + character.Info.MissionsCompletedSinceDeath++; + } + + foreach (KeyValuePair reputationReward in ReputationRewards) + { + if (reputationReward.Key == "location") + { + OriginLocation.Reputation?.AddReputation(reputationReward.Value); + } + else + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); + float prevValue = faction.Reputation.Value; + faction?.Reputation.AddReputation(reputationReward.Value); + } } } @@ -493,12 +521,9 @@ namespace Barotrauma float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); - return reward switch - { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), - None _ => (0, rewardPercentage, sum), - _ => throw new ArgumentOutOfRangeException() - }; + int amount = reward.TryUnwrap(out var a) ? a : 0; + + return ((int)(amount * rewardWeight), rewardPercentage, sum); } protected void ChangeLocationType(LocationTypeChange change) @@ -518,6 +543,8 @@ namespace Barotrauma if (srcIndex == -1) { return; } var location = Locations[srcIndex]; + if (location.LocationTypeChangesBlocked) { return; } + if (change.RequiredDurationRange.X > 0) { location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 9bf5119a3..1c2663dcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -99,7 +99,13 @@ namespace Barotrauma public readonly bool RequireWreck, RequireRuin; + /// + /// If enabled, locations this mission takes place in cannot change their type + /// + public readonly bool BlockLocationTypeChanges; + public readonly bool ShowProgressBar; + public readonly bool ShowProgressInNumbers; public readonly int MaxProgressState; public readonly LocalizedString ProgressBarLabel; @@ -178,6 +184,7 @@ namespace Barotrauma IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); Commonness = element.GetAttributeInt("commonness", 1); AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true); if (element.GetAttribute("difficulty") != null) @@ -187,6 +194,7 @@ namespace Barotrauma } ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); + ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), ""); ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 8351473af..273c38f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -234,6 +234,12 @@ namespace Barotrauma if (!string.IsNullOrEmpty(target.ExistingItemTag)) { var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); + if (GameMain.GameSession?.Missions != null) + { + //don't choose an item that was already chosen as the target for another salvage mission + suitableItems = suitableItems.Where(it => + GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it))); + } switch (target.SpawnPositionType) { case Level.PositionType.Cave: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 786425547..272d38833 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -311,11 +311,14 @@ namespace Barotrauma.Extensions => source .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); - + public static IEnumerable NotNone(this IEnumerable> source) - => source - .OfType>() - .Select(some => some.Value); + { + foreach (var o in source) + { + if (o.TryUnwrap(out var v)) { yield return v; } + } + } public static IEnumerable Successes( this IEnumerable> source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index adb68793d..1a741f3b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -21,7 +21,7 @@ namespace Barotrauma for (int i = 0; i < Submarine.MainSubs.Length; i++) { var sub = Submarine.MainSubs[i]; - if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } + if (sub == null || sub.Info.InitialSuppliesSpawned || sub.Info.IsManuallyOutfitted || !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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index edcf1532a..a2ab60f25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using Barotrauma.Networking; +using System.Collections; #if SERVER using Barotrauma.Networking; #endif @@ -471,6 +472,21 @@ namespace Barotrauma return true; } + public static IEnumerable FindCargoRooms(IEnumerable subs) => subs.SelectMany(s => FindCargoRooms(s)); + + public static IEnumerable FindCargoRooms(Submarine sub) => WayPoint.WayPointList + .Where(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Cargo) + .Select(wp => wp.CurrentHull) + .Distinct(); + + public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) + => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + + public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) + .Select(it => it.GetComponent()) + .Where(c => c != null); + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) { ItemContainer itemContainer = null; @@ -553,8 +569,8 @@ namespace Barotrauma } #endif } - - List availableContainers = new List(); + var connectedSubs = sub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 918ed8ad6..b207870af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -248,11 +248,27 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) + bool hostileOutpost = false; + if (Level.IsLoadedOutpost) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => + if (Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) + { + hostileOutpost = true; + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + var reputation = campaign.Map?.CurrentLocation?.Reputation; + if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + { + hostileOutpost = true; + } + } + } + if (hostileOutpost) + { + spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && + wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) @@ -264,7 +280,6 @@ namespace Barotrauma spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); } } - if (spawnWaypoints == null || !spawnWaypoints.Any()) { spawnWaypoints = mainSubWaypoints; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 4053498ad..a517f3d45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -27,28 +28,20 @@ namespace Barotrauma /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// /// - public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet? characterList = null) + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction) { if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } - characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); - - foreach (Character character in characterList) + bool isHighest = true; + foreach (Faction otherFaction in factions) { - if (character.Info is not { } info) { continue; } + if (otherFaction == faction || otherFaction.Reputation.Value < faction.Reputation.Value) { continue; } - foreach (Faction otherFaction in factions) - { - Identifier factionIdentifier = otherFaction.Prefab.Identifier; - if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f) - { - return factionIdentifier == faction.Prefab.Identifier - ? FactionAffiliation.Positive - : FactionAffiliation.Negative; - } - } + isHighest = false; + break; } - return FactionAffiliation.Neutral; + + return isHighest ? FactionAffiliation.Positive : FactionAffiliation.Negative; } public override string ToString() @@ -88,6 +81,8 @@ namespace Barotrauma public readonly LevelData.LevelType LevelType; public readonly float MinReputation, MaxReputation; public readonly float MinProbability, MaxProbability; + public readonly int MaxDistanceFromFactionOutpost; + public readonly bool DisallowBetweenOtherFactionOutposts; public AutomaticMission(ContentXElement element, string parentDebugName) { @@ -102,6 +97,8 @@ namespace Barotrauma float probability = element.GetAttributeFloat("probability", 0.0f); MinProbability = element.GetAttributeFloat("minprobability", probability); MaxProbability = element.GetAttributeFloat("maxprobability", probability); + MaxDistanceFromFactionOutpost = element.GetAttributeInt("maxdistance", int.MaxValue); + DisallowBetweenOtherFactionOutposts = element.GetAttributeBool(nameof(DisallowBetweenOtherFactionOutposts), false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 5b8f8bacd..5129e6a81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -58,7 +58,7 @@ namespace Barotrauma Value = newReputation; } - public void AddReputation(float reputationChange) + public float GetReputationChangeMultiplier(float reputationChange) { if (reputationChange > 0f) { @@ -68,7 +68,7 @@ namespace Barotrauma reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } - reputationChange *= reputationGainMultiplier; + return reputationGainMultiplier; } else if (reputationChange < 0f) { @@ -78,9 +78,14 @@ namespace Barotrauma reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; } - reputationChange *= reputationLossMultiplier; + return reputationLossMultiplier; } - Value += reputationChange; + return 1.0f; + } + + public void AddReputation(float reputationChange) + { + Value += reputationChange * GetReputationChangeMultiplier(reputationChange); } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index 795cb4680..98a2ebc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -69,7 +69,7 @@ namespace Barotrauma public Option RewardDistributionChanged; public Option BalanceChanged; - public WalletChangedData MergeInto(WalletChangedData other) + public readonly WalletChangedData MergeInto(WalletChangedData other) { other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); @@ -80,32 +80,20 @@ namespace Barotrauma static Option AddOptionalInt(Option a, Option b) { - return a switch - { - Some some1 => b switch - { - Some some2 => Option.Some(some1.Value + some2.Value), - None _ => Option.Some(some1.Value), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - None _ => b switch - { - Some some1 => Option.Some(some1.Value), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - _ => throw new ArgumentOutOfRangeException(nameof(a)) - }; + bool hasValue1 = a.TryUnwrap(out var value1); + bool hasValue2 = b.TryUnwrap(out var value2); + return hasValue1 + ? hasValue2 + ? Option.Some(value1 + value2) + : Option.Some(value1) + : hasValue2 + ? Option.Some(value2) + : Option.None; } static Option TurnToNoneIfZero(Option option) { - return option switch - { - Some s => s.Value == 0 ? Option.None() : option, - None _ => option, - _ => throw new ArgumentOutOfRangeException(nameof(option)) - }; + return option.Bind(i => i == 0 ? Option.None : Option.Some(i)); } } } @@ -223,12 +211,8 @@ namespace Barotrauma }; } - public string GetOwnerLogName() => Owner switch - { - Some { Value: var character } => character.Name, - None _ => "the bank", - _ => throw new ArgumentOutOfRangeException(nameof(Owner)) - }; + public string GetOwnerLogName() + => Owner.TryUnwrap(out var character) ? character.Name : "the bank"; partial void SettingsChanged(Option balanceChanged, Option rewardChanged); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 47e20b5f6..9db5d70b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -13,13 +14,11 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { [NetworkSerialize] - public struct SaveInfo : INetSerializableStruct - { - public string FilePath; - public int SaveTime; - public string SubmarineName; - public string[] EnabledContentPackageNames; - } + public readonly record struct SaveInfo( + string FilePath, + Option SaveTime, + string SubmarineName, + ImmutableArray EnabledContentPackageNames) : INetSerializableStruct; public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; @@ -84,9 +83,9 @@ namespace Barotrauma public bool CheatsEnabled; - public const float HullRepairCostPerDamage = 0.5f, ItemRepairCostPerRepairDuration = 1.0f; + public const float HullRepairCostPerDamage = 0.1f, ItemRepairCostPerRepairDuration = 1.0f; public const int ShuttleReplaceCost = 1000; - public const int MaxHullRepairCost = 2000, MaxItemRepairCost = 2000; + public const int MaxHullRepairCost = 600, MaxItemRepairCost = 2000; protected bool wasDocked; @@ -141,10 +140,19 @@ namespace Barotrauma private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } - //allow managing if no-one with permissions is alive - return - GameMain.NetworkMember.ConnectedClients.Count == 1 || - GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + if (GameMain.NetworkMember.ConnectedClients.Count == 1) { return true; } + + if (GameMain.NetworkMember.GameStarted) + { + //allow managing if no-one with permissions is alive and in-game + return GameMain.NetworkMember.ConnectedClients.None(c => + c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && + (IsOwner(c) || c.HasPermission(permissions))); + } + else + { + return GameMain.NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions)); + } } protected CampaignMode(GameModePreset preset, CampaignSettings settings) @@ -163,21 +171,20 @@ namespace Barotrauma #if CLIENT OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e => { - if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } + if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; } if (changed == 0) { return; } bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; - switch (e.Owner) + if (e.Owner.TryUnwrap(out var owner)) { - case Some { Value: var owner}: - owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); - break; - case None _ when IsSinglePlayer: - Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); - break; + owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); + } + else if (IsSinglePlayer) + { + Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); } string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString(); @@ -408,14 +415,27 @@ namespace Barotrauma } foreach (Faction faction in factions.OrderBy(f => f.Prefab.MenuOrder)) { - if (currentLocation.Faction != faction && currentLocation.SecondaryFaction != faction && - map.SelectedLocation?.Faction != faction && map.SelectedLocation?.SecondaryFaction != faction) - { - continue; - } foreach (var automaticMission in faction.Prefab.AutomaticMissions) { if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { continue; } + + if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.Type == LevelData.LevelType.LocationConnection) + { + if (Map.SelectedConnection.Locations.All(l => l.Faction != null && l.Faction != faction)) + { + continue; + } + } + if (automaticMission.MaxDistanceFromFactionOutpost < int.MaxValue) + { + if (!Map.LocationOrConnectionWithinDistance( + currentLocation, + automaticMission.MaxDistanceFromFactionOutpost, + loc => loc.Faction == faction)) + { + continue; + } + } Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed + TotalPassedLevels)); if (levelData.Type != automaticMission.LevelType) { continue; } float probability = @@ -1012,11 +1032,14 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && character.HumanPrefab is { Identifier: var merchantId }) { character.MerchantIdentifier = merchantId; + map.CurrentLocation?.GetStore(merchantId)?.SetMerchantFaction(character.Faction); } + character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && @@ -1129,7 +1152,7 @@ namespace Barotrauma if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } - if (npc.HumanPrefab?.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.HumanPrefab.Faction) is Faction faction) + if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction) { faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); } @@ -1143,6 +1166,11 @@ namespace Barotrauma } } + public Faction GetFaction(Identifier identifier) + { + return factions.Find(f => f.Prefab.Identifier == identifier); + } + public float GetReputation(Identifier factionIdentifier) { var faction = @@ -1152,6 +1180,12 @@ namespace Barotrauma return faction?.Reputation?.Value ?? 0.0f; } + public FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier) + { + var faction = GetFaction(factionIdentifier); + return Faction.GetPlayerAffiliationStatus(faction); + } + public abstract void Save(XElement element); protected void LoadStats(XElement element) @@ -1267,7 +1301,7 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { - var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1283,7 +1317,6 @@ namespace Barotrauma if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } - if (item.Container?.GetComponent() is { DrawInventory: false }) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; } @@ -1303,15 +1336,29 @@ namespace Barotrauma { // 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.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed) - .Select(it => it.GetComponent()) - .Where(c => c != null) - .ToList(); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + WayPoint wp = WayPoint.WayPointList.FirstOrDefault(wp => wp.SpawnType == SpawnType.Cargo && connectedSubs.Contains(wp.Submarine)); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.FirstOrDefault(h => connectedSubs.Contains(h.Submarine) && !h.IsWetRoom); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + // First move the cargo containers, so that we can reuse them + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + foreach (var (item, oldContainer) in cargoContainers) + { + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); + item.CurrentHull = spawnHull; + item.Submarine = spawnHull.Submarine; + } + // Then move the other items + var cargoRooms = CargoManager.FindCargoRooms(newSub); + List availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList(); foreach (var (item, oldContainer) in itemsToTransfer) { + if (cargoContainers.Contains((item, oldContainer))) { continue; } Item newContainer = null; item.Submarine = newSub; if (item.Container == null) @@ -1320,25 +1367,16 @@ namespace Barotrauma } 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) + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) { - DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); - return; + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - if (spawnHull != null) + else if (cargoContainer.Item.Submarine is Submarine containerSub) { - 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!"); + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; } } string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 2377b386c..31aa8aa9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -11,11 +11,14 @@ namespace Barotrauma { public static CampaignSettings Empty => new CampaignSettings(element: null); +#if CLIENT + public static CampaignSettings CurrentSettings = new CampaignSettings(GameSettings.CurrentConfig.SavedCampaignSettings); +#endif public string Name => "CampaignSettings"; public const string LowerCaseSaveElementName = "campaignsettings"; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Normal", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; [Serialize(true, IsPropertySaveable.Yes)] @@ -53,7 +56,6 @@ namespace Barotrauma return definition.GetInt(StartingBalanceAmount.ToIdentifier()); } return 8000; - } } @@ -65,7 +67,7 @@ namespace Barotrauma { return definition.GetFloat(Difficulty.ToIdentifier()); } - return 0; + return 0; } } @@ -82,7 +84,7 @@ namespace Barotrauma } public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 3; + public const int MaxMissionCountLimit = 10; public const int MinMissionCountLimit = 1; public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index f28cb563c..fa550b175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -46,15 +46,15 @@ namespace Barotrauma public static void Init() { #if CLIENT - Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); - DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); - SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); - TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); + Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true); + DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true); + SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true); + TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true); #endif - Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); - Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); - PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); - MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); + Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false); + Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false); + PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false); + MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9bfbd31c1..97521a5b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -194,7 +194,16 @@ namespace Barotrauma } break; case "metadata": + var prevReputations = Factions.ToDictionary(k => k, v => v.Reputation.Value); CampaignMetadata.Load(subElement); + foreach (var faction in Factions) + { + if (!MathUtils.NearlyEqual(prevReputations[faction], faction.Reputation.Value)) + { + faction.Reputation.OnReputationValueChanged?.Invoke(faction.Reputation); + Reputation.OnAnyReputationValueChanged.Invoke(faction.Reputation); + } + } break; case "upgrademanager": case "pendingupgrades": diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 5074e8ac3..c57e72e3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -306,7 +306,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -324,11 +324,6 @@ 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; Campaign!.TransferItemsOnSubSwitch = transferItems; } @@ -586,9 +581,7 @@ namespace Barotrauma StatusEffect.StopAll(); #if CLIENT -#if !DEBUG - GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; -#endif + GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode; if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } #endif @@ -652,7 +645,7 @@ namespace Barotrauma } } - CreatureMetrics.Instance.RecentlyEncountered.Clear(); + CreatureMetrics.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundDuration = 0.0f; @@ -905,6 +898,7 @@ namespace Barotrauma TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); ObjectiveManager.ResetUI(); + CharacterHUD.ClearBossHealthBars(); #endif SteamAchievementManager.OnRoundEnded(this); @@ -1123,7 +1117,10 @@ namespace Barotrauma XDocument doc = new XDocument(new XElement("Gamesession")); XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); + rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime())); + #warning TODO: after this gets on main, replace savetime with the commented line + //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow)); + rootElement.Add(new XAttribute("version", GameMain.Version)); if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 2b40e6304..4d0a5186a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -14,6 +14,8 @@ namespace Barotrauma public enum NetworkHeader { REQUEST_AFFLICTIONS, + AFFLICTION_UPDATE, + UNSUBSCRIBE_ME, REQUEST_PENDING, ADD_PENDING, REMOVE_PENDING, @@ -295,6 +297,42 @@ namespace Barotrauma static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } + public static void OnAfflictionCountChanged(Character character) => + GameMain.GameSession?.Campaign?.MedicalClinic?.OnAfflictionCountChangedPrivate(character); + + private void OnAfflictionCountChangedPrivate(Character character) + { + if (character is not { CharacterHealth: { } health, Info: { } info }) { return; } + + ImmutableArray afflictions = GetAllAfflictions(health); + +#if CLIENT + if (GameMain.NetworkMember is null) + { + ui?.UpdateAfflictions(new NetCrewMember(info, afflictions)); + } + + ui?.UpdateCrewPanel(); +#elif SERVER + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + continue; + } + + if (sub.Target == info) + { + ServerSend(new NetCrewMember(info, afflictions), + header: NetworkHeader.AFFLICTION_UPDATE, + deliveryMethod: DeliveryMethod.Unreliable, + targetClient: sub.Subscriber); + } + } +#endif + } + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -330,7 +368,7 @@ namespace Barotrauma new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } + new NetAffliction { Identifier = AfflictionPrefab.DamageType, Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index c96413662..5c416862d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -692,11 +692,13 @@ namespace Barotrauma /// Gets the progress that is shown on the store interface. /// Includes values stored in the metadata and , and takes submarine tier and class restrictions into account /// - public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category) + /// Submarine used to determine the upgrade limit. If not defined, will default to the current sub. + public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info = null) { if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); } - return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub()); + int maxLevel = info is null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info); + return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), maxLevel); int GetPendingLevel() { @@ -713,6 +715,14 @@ namespace Barotrauma return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0); } + /// + /// Gets the level of the upgrade that is stored in the metadata. Takes into account the limits of the provided submarine. + /// + public int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info) + { + return Math.Min(GetRealUpgradeLevel(prefab, category), prefab.GetMaxLevel(info)); + } + /// /// Stores the target upgrade level in the campaign metadata. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a9eabf2c6..d644c8267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; @@ -104,15 +105,21 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + + bool forceToSlot = subElement.GetAttributeBool("forcetoslot", false); + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) { - if (item != null && item.ParentInventory != this) + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: forceToSlot, slot: slot, onSpawned: (Item item) => { - string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - } - }); + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); + } } } @@ -171,21 +178,6 @@ namespace Barotrauma (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } - public bool CanBeAutoMovedToCorrectSlots(Item item) - { - if (item == null) { return false; } - foreach (var allowedSlot in item.AllowedSlots) - { - InvSlotType slotsFree = InvSlotType.None; - for (int i = 0; i < slots.Length; i++) - { - if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } - } - if (allowedSlot == slotsFree) { return true; } - } - return false; - } - public override void RemoveItem(Item item) { RemoveItem(item, tryEquipFromSameStack: false); @@ -308,6 +300,8 @@ namespace Barotrauma #endif } + if (item.GetComponent() == null || item.AllowedSlots.None()) { return false; } + bool inSuitableSlot = false; bool inWrongSlot = false; int currentSlot = -1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index d622fa6db..29ac8fea9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -289,7 +289,16 @@ namespace Barotrauma.Items.Components return; } - if (!(joint is WeldJoint)) + if (joint == null) + { + string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." + + " Submarine: " + (item.Submarine?.Info.Name ?? "null") + + ", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null"); + GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return; + } + + if (joint is not WeldJoint) { DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 9eb7e4464..759ea395a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -1,10 +1,10 @@ using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using FarseerPhysics.Dynamics; #if CLIENT using Barotrauma.Lights; #endif @@ -173,6 +173,21 @@ namespace Barotrauma.Items.Components OpenState = isOpen ? 1.0f : 0.0f; } } + public bool IsClosed => !IsOpen; + + /// + /// Is the door opening, but not yet fully opened? Returns false both when it's closed and when it's fully open. + /// + public bool IsOpening => IsOpen && !IsFullyOpen; + + /// + /// Is the door closing, but not yet fully closed? Returns false both when the door is open and when it's fully closed. + /// + public bool IsClosing => IsClosed && !IsFullyClosed; + + public bool IsFullyOpen => IsOpen && OpenState >= 1.0f; + + public bool IsFullyClosed => IsClosed && OpenState <= 0f; [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } @@ -211,6 +226,8 @@ namespace Barotrauma.Items.Components IsHorizontal = element.GetAttributeBool("horizontal", false); canBePicked = element.GetAttributeBool("canbepicked", false); autoOrientGap = element.GetAttributeBool("autoorientgap", false); + + allowedSlots.Clear(); foreach (var subElement in element.Elements()) { @@ -365,7 +382,10 @@ namespace Barotrauma.Items.Components { lastBrokenTime = Timing.TotalTime; //the door has to be restored to 50% health before collision detection on the body is re-enabled - if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f && + + //multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_ + //otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider + if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { IsBroken = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 3dde8c2bc..6620a203c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -58,9 +58,6 @@ namespace Barotrauma.Items.Components set; } - //the angle in which the Character holds the item - protected float holdAngle; - public PhysicsBody Body { get { return item.body ?? body; } @@ -143,6 +140,7 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } + protected float holdAngle; #if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] #else @@ -154,6 +152,18 @@ namespace Barotrauma.Items.Components set { holdAngle = MathHelper.ToRadians(value); } } + protected float aimAngle; +#if DEBUG + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif + public float AimAngle + { + get { return MathHelper.ToDegrees(aimAngle); } + set { aimAngle = MathHelper.ToRadians(value); } + } + private Vector2 swingAmount; #if DEBUG [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] @@ -549,10 +559,17 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { +#if CLIENT if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { + if (!picker.Inventory.CanBeAutoMovedToCorrectSlots(item)) + { + picker.Inventory.FlashAllowedSlots(item, Color.Red); + } return false; } +#endif + bool wasAttached = IsAttached; if (base.OnPicked(picker)) { DeattachFromWall(); @@ -561,7 +578,7 @@ namespace Barotrauma.Items.Components if (GameMain.Server != null && attachable) { item.CreateServerEvent(this); - if (picker != null) + if (picker != null && wasAttached) { GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); } @@ -689,16 +706,22 @@ namespace Barotrauma.Items.Components if (maxAttachableCount == 0) { #if CLIENT - GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); + if (character == Character.Controlled) + { + GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); + } #endif return false; } else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT - GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); + if (character == Character.Controlled) + { + GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); + } #endif - return false; + return false; } } @@ -876,9 +899,13 @@ namespace Barotrauma.Items.Components scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); - if (!aim) + if (aim) { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); + } + else + { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); var rope = GetRope(); if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index cfb71aca1..da760edf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components { UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); if (ac.InWater) { ac.LockFlipping(); @@ -445,7 +445,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } @@ -472,8 +472,8 @@ namespace Barotrauma.Items.Components } if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity)); serverLogger ??= new System.Text.StringBuilder(); serverLogger.Clear(); serverLogger.Append($"{picker?.LogName} used {item.Name}"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 95c40bb0f..9f83723a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components get { return allowedSlots; } } + public bool PickingDone => pickTimer >= PickingTime; + public Character Picker { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 5258d5359..cf93558ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,9 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -145,7 +143,8 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ReloadTimer = Math.Min(reload, 1.0f); + //clamp above 1 to prevent rapid-firing by swapping weapons + ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer); IsActive = true; } @@ -261,7 +260,8 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); + float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter); + var lastProjectile = LastProjectile; if (lastProjectile != projectile) { @@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter)); } Item.RemoveContained(projectile.Item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 3b97b9368..fd24d9673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -100,6 +100,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")] + public bool IgnoreCharacters { get; set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } @@ -313,7 +316,11 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + if (!IgnoreCharacters) + { + collisionCategories |= Physics.CollisionCharacter; + } //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them if (statusEffectLists != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index cd81939f5..422152d7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -42,6 +42,8 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.5f; + public override bool Use(float deltaTime, Character character = null) { //actual throwing logic is handled in Update @@ -59,6 +61,7 @@ namespace Barotrauma.Items.Components base.Drop(dropper); throwState = ThrowState.None; throwAngle = ThrowAngleStart; + Item.ResetWaterDragCoefficient(); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -97,6 +100,7 @@ namespace Barotrauma.Items.Components } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; + Item.ResetWaterDragCoefficient(); } return; } @@ -188,6 +192,7 @@ namespace Barotrauma.Items.Components } item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); + item.WaterDragCoefficient = WaterDragCoefficient; item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again @@ -205,12 +210,12 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower)); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower)); } if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" - ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower); + ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower); } throwState = ThrowState.None; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 7506c6f76..50f5fc39f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -125,8 +125,8 @@ namespace Barotrauma.Items.Components get { return drawable; } set { - if (value == drawable) return; - if (!(this is IDrawableComponent)) + if (value == drawable) { return; } + if (this is not IDrawableComponent) { DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)"); return; @@ -236,10 +236,7 @@ namespace Barotrauma.Items.Components set; } - /// - /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). - /// - [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")] public float CombatPriority { get; private set; } /// @@ -690,7 +687,7 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } - public bool IsLoaded(Character user, bool checkContainedItems = true) => + public bool IsNotEmpty(Character user, bool checkContainedItems = true) => HasRequiredContainedItems(user, addMessage: false) && (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index f91a7089d..3a2e7a9c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -12,20 +12,9 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - class ActiveContainedItem - { - public readonly Item Item; - public readonly StatusEffect StatusEffect; - public readonly bool ExcludeBroken; - public readonly bool ExcludeFullCondition; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) - { - Item = item; - StatusEffect = statusEffect; - ExcludeBroken = excludeBroken; - ExcludeFullCondition = excludeFullCondition; - } - } + readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); + + readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { @@ -63,7 +52,9 @@ namespace Barotrauma.Items.Components public readonly ItemInventory Inventory; private readonly List activeContainedItems = new List(); - + + private readonly List drawableContainedItems = new List(); + private List[] itemIds; //how many items can be contained @@ -114,7 +105,7 @@ namespace Barotrauma.Items.Components [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] public bool DrawInventory { get; @@ -142,6 +133,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool AllowAccess { get; set; } + [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } @@ -348,8 +342,6 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { - item.SetContainedItemPositions(); - int index = Inventory.FindIndex(containedItem); if (index >= 0 && index < slotRestrictions.Length) { @@ -367,6 +359,12 @@ namespace Barotrauma.Items.Components } } + var relatedItem = FindContainableItem(containedItem); + drawableContainedItems.Add(new DrawableContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f)); + if (item.GetComponent() != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); @@ -381,6 +379,7 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -393,6 +392,7 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); + drawableContainedItems.RemoveAll(i => i.Item == containedItem); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -483,8 +483,8 @@ namespace Barotrauma.Items.Components item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter); item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); item.GetComponent()?.Equip(ownerCharacter); - autoInjectCooldown = AutoInjectInterval; } + autoInjectCooldown = AutoInjectInterval; } } @@ -509,10 +509,18 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); - if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) + { + effect.Apply(ActionType.OnContaining, deltaTime, item, character); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { @@ -534,12 +542,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { - if (!DrawInventory) { return false; } + if (!AllowAccess) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -575,7 +583,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - if (!DrawInventory) { return false; } + if (!AllowAccess) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) @@ -756,54 +764,50 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (Item contained in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(contained); - if (relatedItem != null) + if (contained.ItemPos.HasValue) { - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.Position; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; } } - } + } - if (contained.body != null) + if (contained.Item.body != null) { try { Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); float rotation = itemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - rotation = MathHelper.ToRadians(relatedItem.Rotation); + rotation = MathHelper.ToRadians(contained.Rotation); } if (item.body != null) { @@ -814,29 +818,29 @@ namespace Barotrauma.Items.Components { rotation += -item.RotationRad; } - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); - contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); - contained.body.UpdateDrawPosition(); + contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); + contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation); + contained.Item.body.UpdateDrawPosition(); } catch (Exception e) { DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); - GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, + GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name, GameAnalyticsManager.ErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } - contained.body.Submarine = item.Submarine; + contained.Item.body.Submarine = item.Submarine; } - contained.Rect = + contained.Item.Rect = new Rectangle( - (int)(itemPos.X - contained.Rect.Width / 2.0f), - (int)(itemPos.Y + contained.Rect.Height / 2.0f), - contained.Rect.Width, contained.Rect.Height); + (int)(itemPos.X - contained.Item.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Item.Rect.Height / 2.0f), + contained.Item.Rect.Width, contained.Item.Rect.Height); - contained.Submarine = item.Submarine; - contained.CurrentHull = item.CurrentHull; - contained.SetContainedItemPositions(); + contained.Item.Submarine = item.Submarine; + contained.Item.CurrentHull = item.CurrentHull; + contained.Item.SetContainedItemPositions(); i++; if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index d6ae48ec5..e3d29d09b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -116,7 +116,7 @@ namespace Barotrauma.Items.Components { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * voltageFactor; - float condition = item.Condition / item.MaxCondition; + float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); UpdateAITargets(noise); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index f8875bd78..b7b475055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -6,6 +6,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Voronoi2; @@ -13,6 +14,21 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { + const int SpreadCounterWrapAround = 256; + + private static readonly ImmutableArray spreadPool; + static Projectile() + { + MTRandom random = new MTRandom(0); + spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + } + + public static float GetSpreadFromPool(int seed) + { + if (seed < 0) { seed = -seed; } + return spreadPool[seed % SpreadCounterWrapAround]; + } + struct HitscanResult { public Fixture Fixture; @@ -41,10 +57,14 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.1f; + private readonly Queue impactQueue = new Queue(); private bool removePending; + public byte SpreadCounter { get; private set; } + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -112,27 +132,34 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")] public bool StickToCharacters { get; set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")] public bool StickToStructures { get; set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")] public bool StickToItems { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")] + public bool StickToDoors + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")] public bool StickToDeflective { @@ -273,6 +300,8 @@ namespace Barotrauma.Items.Components return; } + SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); @@ -352,7 +381,7 @@ namespace Barotrauma.Items.Components { #if SERVER launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true)); + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); #endif } } @@ -376,8 +405,9 @@ namespace Barotrauma.Items.Components } else { - launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter)); } + SpreadCounter++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) @@ -395,7 +425,6 @@ namespace Barotrauma.Items.Components item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); DoLaunch(launchDir * modifiedLaunchImpulse); - System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); } } User = character; @@ -416,6 +445,7 @@ namespace Barotrauma.Items.Components } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; @@ -450,6 +480,7 @@ namespace Barotrauma.Items.Components Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); + Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; //set the velocity of the body because the OnProjectileCollision method @@ -467,36 +498,36 @@ namespace Barotrauma.Items.Components Vector2 rayEndWorld = rayStartWorld + dir * worldDist; List hits = new List(); - hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine)); if (item.Submarine != null) { //shooting indoors, do a hitscan outside as well hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null)); - //also in the coordinate space of docked subs - foreach (Submarine dockedSub in item.Submarine.DockedTo) - { - if (dockedSub == item.Submarine) { continue; } - hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub)); - } + //do a hitscan in other subs' coordinate spaces + RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition); } else + { + RayCastInOtherSubs(rayStart, rayEnd); + } + + void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd) { //shooting outdoors, see if we can hit anything inside a sub foreach (Submarine submarine in Submarine.Loaded) { + if (submarine == item.Submarine) { continue; } var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine); //transform back to world coordinates for (int i = 0; i < inSubHits.Count; i++) { inSubHits[i] = new HitscanResult( - inSubHits[i].Fixture, - inSubHits[i].Point + submarine.SimPosition, - inSubHits[i].Normal, + inSubHits[i].Fixture, + inSubHits[i].Point + submarine.SimPosition, + inSubHits[i].Normal, inSubHits[i].Fraction); } - hits.AddRange(inSubHits); } } @@ -508,6 +539,7 @@ namespace Barotrauma.Items.Components { var h = hits[i]; item.SetTransform(h.Point, rotation); + item.UpdateTransform(); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitCount++; @@ -675,6 +707,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { + Item.ResetWaterDragCoefficient(); if (dropper != null) { DisableProjectileCollisions(); @@ -941,7 +974,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } @@ -1016,8 +1049,8 @@ namespace Barotrauma.Items.Components } if (GameMain.NetworkMember is { IsServer: true } server) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition)); } } else @@ -1026,8 +1059,8 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User); if (GameMain.NetworkMember is { IsServer: true } server) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition)); } } } @@ -1035,13 +1068,12 @@ namespace Barotrauma.Items.Components target.Body.ApplyLinearImpulse(velocity * item.body.Mass); target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f); - if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) + if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { DisableProjectileCollisions(); } - if (attackResult.AppliedDamageModifiers != null && - (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) + if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective) { item.body.LinearVelocity *= deflectedSpeedMultiplier; } @@ -1051,7 +1083,7 @@ namespace Barotrauma.Items.Components ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToItems && target.Body.UserData is Item)))) + (target.Body.UserData is Item i && (i.GetComponent() != null ? StickToDoors : StickToItems))))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e5604f449..52e928c80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,12 +1,11 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +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.Abilities; namespace Barotrauma.Items.Components { @@ -17,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int updateDeteriorationCounter; + private const int UpdateDeteriorationInterval = 10; + private int prevSentConditionValue; private string conditionSignal; @@ -404,26 +406,11 @@ namespace Barotrauma.Items.Components #endif } } - if (!ShouldDeteriorate()) { return; } - if (item.Condition > 0.0f) + updateDeteriorationCounter++; + if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { - if (deteriorationTimer > 0.0f) - { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); -#if SERVER - if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } -#endif - } - return; - } - - if (item.ConditionPercentage > MinDeteriorationCondition) - { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); - item.Condition -= deteriorationSpeed * deltaTime; - } + UpdateDeterioration(deltaTime * UpdateDeteriorationInterval); + updateDeteriorationCounter = 0; } return; } @@ -559,6 +546,30 @@ namespace Barotrauma.Items.Components } } + private void UpdateDeterioration(float deltaTime) + { + if (item.Condition <= 0.0f) { return; } + if (!ShouldDeteriorate()) { return; } + + if (deteriorationTimer > 0.0f) + { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); +#if SERVER + if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } +#endif + } + return; + } + + if (item.ConditionPercentage > MinDeteriorationCondition) + { + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; + } + } + private float GetMaxRepairConditionMultiplier(Character character) { if (character == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index bb38957ea..d15979d30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -302,33 +302,16 @@ namespace Barotrauma.Items.Components var sourceBody = GetBodyToPull(source); if (sourceBody != null) { - var targetBody = GetBodyToPull(target); - if (targetBody != null && targetBody.UserData is not Character) + if (user != null && user.InWater) { - sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); - } - float forceMultiplier = 1; - if (user != null) - { - user.AnimController.Hang(); - if (user.InWater) + if (user.IsRagdolled) { - if (user.IsRagdolled) - { - forceMultiplier = 0; - } - } - else - { - forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f; - // Prevents too easy smashing to the walls - forceDir.X /= 4; - // Prevents rubberbanding up and down - if (forceDir.Y < 0) - { - forceDir.Y = 0; - } + // Reel in towards the target. + user.AnimController.Hang(); + float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce; + sourceBody.ApplyForce(forceDir * force); } + // Take the target velocity into account. if (targetCharacter != null) { var myCollider = user.AnimController.Collider; @@ -341,9 +324,15 @@ namespace Barotrauma.Items.Components } } } + else + { + var targetBody = GetBodyToPull(target); + if (targetBody != null) + { + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); + } + } } - float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier; - sourceBody.ApplyForce(forceDir * force); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 56c6e2e11..ceced8a5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -304,9 +304,12 @@ namespace Barotrauma.Items.Components } #if SERVER //make sure the clients know about the states of the checkboxes and text fields - if (item.Submarine == null || !item.Submarine.Loading) + if (customInterfaceElementList.Any()) { - item.CreateServerEvent(this); + if (item.Submarine == null || !item.Submarine.Loading) + { + item.CreateServerEvent(this); + } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 3b79208c7..eb9d3990f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -187,6 +187,15 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] + public bool AlphaBlend + { + get; + set; + } + + public float TemporaryFlickerTimer; + public override void Move(Vector2 amount, bool ignoreContacts = false) { #if CLIENT @@ -205,7 +214,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -239,6 +248,7 @@ namespace Barotrauma.Items.Components SetLightSourceState(IsActive); turret = item.GetComponent(); #if CLIENT + Drawable = AlphaBlend && Light.LightSprite != null; if (Screen.Selected.IsEditor) { OnMapLoaded(); @@ -311,8 +321,10 @@ namespace Barotrauma.Items.Components return; } + TemporaryFlickerTimer -= deltaTime; + //currPowerConsumption = powerConsumption; - if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) + if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f)) { #if CLIENT if (Voltage > 0.1f) @@ -364,7 +376,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled); + SetLightSourceState(Light.Enabled, lightBrightness); #endif prevColorSignal = signal.value; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6ebfe3fc8..93527069d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 1240fb9cb..f596fd3d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components private string prevSignal; - private readonly int[] channelMemory = new int[ChannelMemorySize]; + private int[] channelMemory = new int[ChannelMemorySize]; private Connection signalInConnection; private Connection signalOutConnection; @@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components { list.Add(this); IsActive = true; - channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + if (channelMemory.Length != ChannelMemorySize) + { + DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing..."); + Array.Resize(ref channelMemory, ChannelMemorySize); + } } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 863210c6b..09dfe9080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { - partial class WireSection + public partial class WireSection { private Vector2 start; private Vector2 end; @@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public static IEnumerable ExtractNodes(XElement element) { - base.Load(componentElement, usePrefabValues, idRemap); - - string nodeString = componentElement.GetAttributeString("nodes", ""); - if (nodeString == "") return; + string nodeString = element.GetAttributeString("nodes", ""); + if (nodeString.IsNullOrWhiteSpace()) { yield break; } string[] nodeCoords = nodeString.Split(';'); for (int i = 0; i < nodeCoords.Length / 2; i++) { - float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x); - float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y); - nodes.Add(new Vector2(x, y)); + float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x); + float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y); + yield return new Vector2(x, y); } + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + + nodes.AddRange(ExtractNodes(componentElement)); Drawable = nodes.Any(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index b8c60dd0c..8455a3b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -59,8 +59,9 @@ namespace Barotrauma.Items.Components private float aiTargetingGraceTimer; private float aiFindTargetTimer; - private Character currentTarget; - const float aiFindTargetInterval = 5.0f; + private ISpatialEntity currentTarget; + private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -73,6 +74,8 @@ namespace Barotrauma.Items.Components private List lightComponents; + private readonly bool isSlowTurret; + public float Rotation { get { return rotation; } @@ -320,33 +323,36 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable] public bool AutoOperate { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly?"), Editable] - public float RandomAimAmount { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable] + public float RandomAimAmount { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable] - public float RandomAimMinTime { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable] + public float RandomAimMinTime { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable] - public float RandomAimMaxTime { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable] + public float RandomAimMaxTime { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable] public bool RandomMovement { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret always aim at targets without delay?"), Editable] - public bool IgnoreAimDelay { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable] + public bool AimDelay { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable] public bool TargetCharacters { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target monsters?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable] public bool TargetMonsters { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target humans (or pets)"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable] public bool TargetHumans { get; set; } [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable] public bool TargetSubmarines { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable] + public bool TargetItems { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] public Identifier FriendlyTag { get; private set; } @@ -379,6 +385,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; + isSlowTurret = item.HasTag("slowturret"); InitProjSpecific(element); } @@ -940,7 +947,7 @@ namespace Barotrauma.Items.Components } private float waitTimer; - private float disorderTimer; + private float randomAimTimer; private float prevTargetRotation; private float updateTimer; @@ -950,10 +957,6 @@ namespace Barotrauma.Items.Components { IsActive = true; - bool targetCharacters = TargetCharacters || TargetHumans || TargetMonsters; - bool targetHumans = TargetCharacters && TargetHumans; - bool targetMonsters = TargetCharacters && TargetMonsters; - if (friendlyTag.IsEmpty) { friendlyTag = FriendlyTag; @@ -977,7 +980,7 @@ namespace Barotrauma.Items.Components updateTimer -= deltaTime; } - if (!IgnoreAimDelay && waitTimer > 0) + if (AimDelay && waitTimer > 0) { waitTimer -= deltaTime; return; @@ -987,30 +990,34 @@ namespace Barotrauma.Items.Components float shootDistance = AIRange; ISpatialEntity target = null; float closestDist = shootDistance * shootDistance; - if (targetCharacters) + if (TargetCharacters) { foreach (var character in Character.CharacterList) { - if (character == null || character.Removed || character.IsDead) { continue; } - if (!friendlyTag.IsEmpty && (character.SpeciesName.Equals(friendlyTag) || character.Group.Equals(friendlyTag))) { continue; } - bool isHuman = character.IsHuman || character.Group == CharacterPrefab.HumanSpeciesName; - if (isHuman) - { - if (!targetHumans) - { - // Don't target humans if not defined to. - continue; - } - } - else if (!targetMonsters) - { - // Don't target other creatures if not defined to. - continue; - } + if (!IsValidTarget(character)) { continue; } + float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority; + if (priority <= 0) { continue; } + if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + if (!CheckTurretAngle(character.WorldPosition)) { continue; } target = character; - closestDist = dist; + closestDist = dist / priority; + } + } + if (TargetItems) + { + foreach (Item targetItem in Item.ItemList) + { + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDist) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + target = targetItem; + closestDist = dist / priority; } } if (TargetSubmarines) @@ -1020,8 +1027,11 @@ namespace Barotrauma.Items.Components closestDist = maxDistance * maxDistance; foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player) { continue; } if (sub == Item.Submarine) { continue; } + if (item.Submarine != null) + { + if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } + } float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } closestSub = sub; @@ -1035,6 +1045,7 @@ namespace Barotrauma.Items.Components if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + // Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway. target = hull; closestDist = dist; } @@ -1051,22 +1062,23 @@ namespace Barotrauma.Items.Components return; } - if (!IgnoreAimDelay) + if (AimDelay) { if (RandomAimAmount > 0) { - if (disorderTimer < 0) + if (randomAimTimer < 0) { - // Random disorder - disorderTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); + // Random disorder or other flaw in the targeting. + randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); waitTimer = Rand.Range(0.25f, 1f); - targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-RandomAimAmount, RandomAimAmount)); + float randomAim = MathHelper.ToRadians(RandomAimAmount); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim)); updatePending = true; return; } else { - disorderTimer -= deltaTime; + randomAimTimer -= deltaTime; } } } @@ -1264,18 +1276,19 @@ namespace Barotrauma.Items.Components bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (currentTarget.Removed || currentTarget.IsDead) + if (!IsValidTarget(currentTarget)) { currentTarget = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; } } - - if (aiFindTargetTimer <= 0.0f || currentTarget == null) + if (aiFindTargetTimer <= 0.0f) { foreach (Character enemy in Character.CharacterList) { - // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled) { continue; } + if (!IsValidTarget(enemy)) { continue; } + float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority; + if (priority <= 0) { continue; } if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } @@ -1292,30 +1305,53 @@ namespace Barotrauma.Items.Components // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } } + targetPos = enemy.WorldPosition; closestEnemy = enemy; - closestDistance = dist; + closestDistance = dist / priority; + currentTarget = closestEnemy; } - currentTarget = closestEnemy; - aiFindTargetTimer = aiFindTargetInterval; - } - else - { - closestEnemy = currentTarget; - } - - if (closestEnemy != null) - { - targetPos = closestEnemy.WorldPosition; - //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item)) + foreach (Item targetItem in Item.ItemList) { - targetPos = closestEnemy.CurrentHull.WorldPosition; + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDistance) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + targetPos = targetItem.WorldPosition; + closestDistance = dist / priority; + // Override the target character so that we can target the item instead. + closestEnemy = null; + currentTarget = targetItem; + } + if (currentTarget == null) + { + aiFindTargetTimer = CrewAIFindTargetMinInverval; + } + else + { + aiFindTargetTimer = CrewAiFindTargetMaxInterval; + } + } + else if (currentTarget != null) + { + targetPos = currentTarget.WorldPosition; + } + bool iceSpireSpotted = false; + // Adjust the target character position (limb or submarine) + if (currentTarget is Character targetCharacter) + { + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + { + targetPos = targetCharacter.CurrentHull.WorldPosition; } else { // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. float closestDist = closestDistance; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } @@ -1329,13 +1365,14 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot + // Not close enough to shoot. + currentTarget = null; closestEnemy = null; targetPos = null; } } } - else if (item.Submarine != null && Level.Loaded != null) + else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { // Check ice spires shootDistance = AIRange * item.OffsetOnSelectedMultiplier; @@ -1345,50 +1382,49 @@ namespace Barotrauma.Items.Components if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { - if (cell.DoesDamage) + if (!cell.DoesDamage) { continue; } + foreach (var edge in cell.Edges) { - foreach (var edge in cell.Edges) + Vector2 p1 = edge.Point1 + cell.Translation; + Vector2 p2 = edge.Point2 + cell.Translation; + Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); + if (!CheckTurretAngle(closestPoint)) { - Vector2 p1 = edge.Point1 + cell.Translation; - Vector2 p2 = edge.Point2 + cell.Translation; - Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + // The closest point can't be targeted -> get a point directly in front of the turret + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { - // The closest point can't be targeted -> get a point directly in front of the turret - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); - if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) - { - closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } - } - else - { - continue; - } + closestPoint = intersection; + if (!CheckTurretAngle(closestPoint)) { continue; } } - float dist = Vector2.Distance(closestPoint, item.WorldPosition); - - //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell - closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); - - if (dist > AIRange + 1000) { continue; } - float dot = 0; - if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + else { - dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); - } - float minAngle = 0.5f; - if (dot < minAngle && dist > 1000) - { - // The sub is not moving towards the target and it's not very close to the turret either -> ignore continue; } - // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); - if (dist > closestDistance) { continue; } - targetPos = closestPoint; - closestDistance = dist; } + float dist = Vector2.Distance(closestPoint, item.WorldPosition); + + //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell + closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); + + if (dist > AIRange + 1000) { continue; } + float dot = 0; + if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + { + dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); + } + float minAngle = 0.5f; + if (dot < minAngle && dist > 1000) + { + // The sub is not moving towards the target and it's not very close to the turret either -> ignore + continue; + } + // Allow targeting farther when heading towards the spire (up to 1000 px) + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); + if (dist > closestDistance) { continue; } + targetPos = closestPoint; + closestDistance = dist; + iceSpireSpotted = true; } } } @@ -1404,13 +1440,13 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) + if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) { character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, identifier: "identifiedtargetspotted".ToIdentifier(), @@ -1423,17 +1459,17 @@ namespace Barotrauma.Items.Components minDurationBetweenSimilar: 5.0f); } } - else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } - character.AddEncounter(closestEnemy); + CreatureMetrics.AddEncounter(closestEnemy.SpeciesName); } character.AIController.SelectTarget(closestEnemy.AiTarget); } - else if (closestEnemy == null && character.IsOnPlayerTeam) + else if (iceSpireSpotted && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, identifier: "icespirespotted".ToIdentifier(), @@ -1496,6 +1532,54 @@ namespace Barotrauma.Items.Components return 0; } + // Not exahustive, but helps to get rid of some code duplication + private static bool IsValidTarget(ISpatialEntity target) + { + if (target == null) { return false; } + if (target is Character targetCharacter) + { + if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0) + { + return false; + } + } + else if (target is Item targetItem) + { + if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame) + { + return false; + } + if (targetItem.Submarine != null) + { + return false; + } + } + return true; + } + + private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag) + { + if (!friendlyTag.IsEmpty) + { + if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; } + } + bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName; + if (isHuman) + { + if (item.Submarine != null) + { + // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). + return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; + } + return TargetHumans; + } + else + { + // Shouldn't check the team here, because all the enemies are in the same team (None). + return TargetMonsters; + } + } + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) { if (targetBody == null) { return false; } @@ -1508,7 +1592,7 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null) + if (targetCharacter != null && !targetCharacter.Removed) { if (user != null) { @@ -1517,27 +1601,25 @@ namespace Barotrauma.Items.Components return false; } } - if (!friendlyTag.IsEmpty) + else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag)) { - if (targetCharacter.SpeciesName.Equals(friendlyTag) || targetCharacter.Group.Equals(friendlyTag)) - { - return false; - } + // Note that Thalamus runs this even when AutoOperate is false. + return false; } } else { if (targetBody.UserData is ISpatialEntity e) { - if (e is Structure s && s.Indestructible) { return false; } - Submarine sub = e.Submarine ?? e as Submarine; + if (e is Structure { Indestructible: true }) { return false; } if (!targetSubmarines && e is Submarine) { return false; } - if (sub == null) { return false; } + Submarine sub = e.Submarine ?? e as Submarine; + if (sub == null) { return true; } if (sub == Item.Submarine) { return false; } if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } if (sub.TeamID == Item.Submarine.TeamID) { return false; } } - else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true }) { // Hit something else, probably a level wall return false; @@ -1548,7 +1630,7 @@ namespace Barotrauma.Items.Components private Body CheckLineOfSight(Vector2 start, Vector2 end) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile; Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, customPredicate: (Fixture f) => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index e61f4cfa7..fc8e6dd4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -44,7 +44,16 @@ namespace Barotrauma } public LimbType Limb { get; private set; } public bool HideLimb { get; private set; } - public bool HideOtherWearables { get; private set; } + + public enum ObscuringMode + { + None, + Hide, + AlphaClip + } + public ObscuringMode ObscureOtherWearables { get; private set; } + public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide; + public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip; public bool CanBeHiddenByOtherWearables { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } @@ -130,7 +139,7 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - HideOtherWearables = false; + ObscureOtherWearables = ObscuringMode.None; InheritLimbDepth = true; InheritScale = true; InheritOrigin = true; @@ -202,7 +211,16 @@ namespace Barotrauma Sprite = new Sprite(SourceElement, file: SpritePath); Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); - HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false); + + foreach (var mode in Enum.GetValues()) + { + if (mode == ObscuringMode.None) { continue; } + if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false)) + { + ObscureOtherWearables = mode; + } + } + CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); var scale = SourceElement.GetAttribute("inheritscale"); @@ -509,7 +527,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (picker.Removed) + if (picker == null || picker.Removed) { IsActive = false; return; @@ -519,7 +537,7 @@ namespace Barotrauma.Items.Components if (item.GetComponent() is not { IsActive: true }) { item.SetTransform(picker.SimPosition, 0.0f); - } + } item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 447b99409..5a08c3838 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -141,7 +141,18 @@ namespace Barotrauma } } if (items.Contains(item)) { return; } - items.Add(item); + + //keep lowest-condition items at the top of the stack + int index = 0; + for (int i = 0; i < items.Count; i++) + { + if (items[i].Condition > item.Condition) + { + break; + } + index++; + } + items.Insert(index, item); } /// @@ -585,6 +596,8 @@ namespace Barotrauma item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); + //update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies) + item.body.Update(); } #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2ad916128..301af1341 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -99,7 +99,18 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; - private float waterDragCoefficient; + private readonly float originalWaterDragCoefficient; + private float? overrideWaterDragCoefficient; + public float WaterDragCoefficient + { + get => overrideWaterDragCoefficient ?? originalWaterDragCoefficient; + set => overrideWaterDragCoefficient = value; + } + + /// + /// Removes the override value -> falls back to using the original value defined in the xml. + /// + public void ResetWaterDragCoefficient() => overrideWaterDragCoefficient = null; public readonly XElement StaticBodyConfig; @@ -900,7 +911,7 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition = Prefab.Health; + condition = MaxCondition = prevCondition = Prefab.Health; ConditionPercentage = 100.0f; lastSentCondition = condition; @@ -990,6 +1001,7 @@ namespace Barotrauma case "infectedsprite": case "damagedinfectedsprite": case "swappableitem": + case "skillrequirementhint": break; case "staticbody": StaticBodyConfig = subElement; @@ -1002,13 +1014,6 @@ namespace Barotrauma if (ic == null) break; AddComponent(ic); - - if (ic is IDrawableComponent && ic.Drawable) - { - drawableComponents.Add(ic as IDrawableComponent); - hasComponentsToDraw = true; - } - if (ic is Repairable) repairables.Add((Repairable)ic); break; } } @@ -1023,6 +1028,14 @@ namespace Barotrauma } } + if (ic is Repairable repairable) { repairables.Add(repairable); } + + if (ic is IDrawableComponent && ic.Drawable) + { + drawableComponents.Add(ic as IDrawableComponent); + hasComponentsToDraw = true; + } + if (ic.statusEffectLists == null) { continue; } if (ic.InheritStatusEffects) { @@ -1057,8 +1070,7 @@ namespace Barotrauma if (body != null) { body.Submarine = submarine; - waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", - GetComponent() != null || GetComponent() != null ? 0.1f : 1.0f); + originalWaterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", 5.0f); } //cache connections into a dictionary for faster lookups @@ -1655,7 +1667,7 @@ namespace Barotrauma if (effect.TargetSlot > -1) { - if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + if (!OwnInventory.GetItemsAt(effect.TargetSlot).Contains(containedItem)) { continue; } } hasTargets = true; @@ -1733,7 +1745,7 @@ namespace Barotrauma { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } - float damageAmount = attack.GetItemDamage(deltaTime); + float damageAmount = attack.GetItemDamage(deltaTime, Prefab.ItemDamageMultiplier); Condition -= damageAmount; if (damageAmount >= Prefab.OnDamagedThreshold) @@ -1761,6 +1773,7 @@ namespace Barotrauma RecalculateConditionValues(); + bool wasPreviousConditionChanged = false; if (condition == 0.0f && prevCondition > 0.0f) { //Flag connections to be updated as device is broken @@ -1773,6 +1786,8 @@ namespace Barotrauma } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif + // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow. + SetPreviousCondition(); ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } else if (condition > 0.0f && prevCondition <= 0.0f) @@ -1803,9 +1818,18 @@ namespace Barotrauma } } - LastConditionChange = condition - prevCondition; - ConditionLastUpdated = Timing.TotalTime; - prevCondition = condition; + if (!wasPreviousConditionChanged) + { + SetPreviousCondition(); + } + + void SetPreviousCondition() + { + LastConditionChange = condition - prevCondition; + ConditionLastUpdated = Timing.TotalTime; + prevCondition = condition; + wasPreviousConditionChanged = true; + } static void flagChangedConnections(Dictionary connections) { @@ -1991,8 +2015,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = IsInWater(); - bool waterProof = WaterProof; + inWater = IsInWater() && !WaterProof; if (inWater) { //the item has gone through the surface of the water @@ -2007,15 +2030,19 @@ namespace Barotrauma } Item container = this.Container; - while (!waterProof && container != null) + while (container != null) { - waterProof = container.WaterProof; + if (container.WaterProof) + { + inWater = false; + break; + } container = container.Container; } } if (hasWaterStatusEffects && condition > 0.0f) { - ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); } } else @@ -2141,7 +2168,7 @@ namespace Barotrauma Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront); float speed = frontVel.Length(); - float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity; + float drag = speed * speed * WaterDragCoefficient * volume * Physics.NeutralDensity; //very small drag on active projectiles to prevent affecting their trajectories much if (body.FarseerBody.IsBullet) { drag *= 0.1f; } Vector2 dragVec = -frontVel / speed * drag; @@ -2711,7 +2738,7 @@ namespace Barotrauma return; } - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2728,7 +2755,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character); + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2742,7 +2769,7 @@ namespace Barotrauma public void SecondaryUse(float deltaTime, Character character = null) { - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2778,6 +2805,13 @@ namespace Barotrauma if (!UseInHealthInterface) { return; } #if CLIENT + if (user == Character.Controlled) + { + if (HealingCooldown.IsOnCooldown) { return; } + + HealingCooldown.PutOnCooldown(); + } + if (GameMain.Client != null) { GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb)); @@ -2798,13 +2832,13 @@ namespace Barotrauma #endif ic.WasUsed = true; - ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: character, user: user); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb, useTarget: character)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb, useTarget: character)); } if (ic.DeleteOnUse) { remove = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 1bf731566..a4a8fc521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1,16 +1,33 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Extensions; using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { + readonly struct SkillRequirementHint + { + public readonly Identifier Skill; + public readonly float Level; + public readonly LocalizedString SkillName; + + public LocalizedString GetFormattedText(int skillLevel, string levelColorTag) => + $"{SkillName} {Level} (‖color:{levelColorTag}‖{skillLevel}‖color:end‖)"; + + public SkillRequirementHint(ContentXElement element) + { + Skill = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Level = element.GetAttributeFloat("level", 0); + SkillName = TextManager.Get("skillname." + Skill); + } + } + readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; @@ -443,6 +460,8 @@ namespace Barotrauma //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public ImmutableArray PreferredContainers { get; private set; } + public ImmutableArray SkillRequirementHints { get; private set; } + public SwappableItem SwappableItem { get; @@ -568,9 +587,11 @@ namespace Barotrauma public ImmutableDictionary LevelQuantity { get; private set; } - public bool CanSpriteFlipX { get; private set; } + private bool canSpriteFlipX; + public override bool CanSpriteFlipX => canSpriteFlipX; - public bool CanSpriteFlipY { get; private set; } + private bool canSpriteFlipY; + public override bool CanSpriteFlipY => canSpriteFlipY; /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. @@ -658,6 +679,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float ExplosionDamageMultiplier { get; private set; } + [Serialize(1f, IsPropertySaveable.No)] + public float ItemDamageMultiplier { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool DamagedByProjectiles { get; private set; } @@ -767,6 +791,21 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.No)] public bool ShowHealthBar { get; private set; } + [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")] + public float BotPriority { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool ShowNameInHealthBar { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description:"Should the bots shoot at this item with turret or not? Disabled by default.")] + public bool IsAITurretTarget { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with turrets? Defaults to 1. Distance to the target affects the decision making.")] + public float AITurretPriority { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] + public float AISlowTurretPriority { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -869,6 +908,15 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); LoadDescription(ConfigElement); + var skillRequirementHints = new List(); + foreach (var skillRequirementHintElement in ConfigElement.GetChildElements("SkillRequirementHint")) + { + skillRequirementHints.Add(new SkillRequirementHint(skillRequirementHintElement)); + } + if (skillRequirementHints.Any()) + { + SkillRequirementHints = skillRequirementHints.ToImmutableArray(); + } var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); @@ -884,8 +932,8 @@ namespace Barotrauma case "sprite": string spriteFolder = GetTexturePath(subElement, variantOf); - CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); - CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); + canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); + canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); if (subElement.GetAttribute("sourcerect") == null && @@ -936,14 +984,20 @@ namespace Barotrauma AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); - foreach (XElement deconstructItem in subElement.Elements()) + foreach (XElement itemElement in subElement.Elements()) { - if (deconstructItem.Attribute("name") != null) + if (itemElement.Attribute("name") != null) { DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier)); + var deconstructItem = new DeconstructItem(itemElement, Identifier); + if (deconstructItem.ItemIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier."); + continue; + } + deconstructItems.Add(deconstructItem); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count); break; @@ -1336,11 +1390,43 @@ namespace Barotrauma } public Identifier VariantOf { get; } - + public ItemPrefab ParentPrefab { get; set; } + public void InheritFrom(ItemPrefab parent) { - ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage); ParseConfigElement(parent); + + void CheckXML(XElement originalElement, XElement variantElement, XElement result) + { + if (result == null) { return; } + if (result.Name.ToIdentifier() == "RequiredItem" && + result.Parent?.Name.ToIdentifier() == "Fabricate") + { + int originalAmount = originalElement.GetAttributeInt("amount", 1); + Identifier originalIdentifier = originalElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (variantElement == null) + { + //if the variant defines some fabrication requirements, we probably don't want to inherit anything extra from the base item? + if (this.originalElement.GetChildElement("Fabricate")?.GetChildElement("RequiredItem") != null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " + + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements."); + } + return; + } + + Identifier resultIdentifier = result.GetAttributeIdentifier("identifier", Identifier.Empty); + if (originalAmount > 1 && variantElement.GetAttribute("amount") == null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " + + $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+ + "Specify the amount in the variant to fix this."); + } + } + } } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 7d5ab26b4..11e69f606 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -66,7 +66,7 @@ namespace Barotrauma /// /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. /// - public bool? Hide; + public bool Hide; public float Rotation; @@ -224,9 +224,9 @@ namespace Barotrauma new XAttribute("rotation", Rotation), new XAttribute("setactive", SetActive)); - if (Hide.HasValue) + if (Hide) { - element.Add(new XAttribute(nameof(Hide), Hide.Value)); + element.Add(new XAttribute(nameof(Hide), true)); } if (ItemPos.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index beace2553..92effd495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -18,9 +18,9 @@ namespace Barotrauma public const ushort ReservedIDStart = ushort.MaxValue - 3; - public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values + public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values - private static Dictionary dictionary = new Dictionary(); + private static readonly Dictionary dictionary = new Dictionary(); public static IReadOnlyCollection GetEntities() { return dictionary.Values; @@ -85,6 +85,28 @@ namespace Barotrauma this.Submarine = submarine; spawnTime = Timing.TotalTime; + if (dictionary.Count >= MaxEntityCount) + { + Dictionary entityCounts = new Dictionary(); + foreach (var entity in dictionary) + { + if (entity.Value is MapEntity me) + { + if (entityCounts.ContainsKey(me.Prefab.Identifier)) + { + entityCounts[me.Prefab.Identifier]++; + } + else + { + entityCounts[me.Prefab.Identifier] = 1; + } + } + } + string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " + + string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}")); + throw new Exception(errorMsg); + } + //give a unique ID ID = DetermineID(id, submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 1ca3b27bf..2ce9bd0e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -127,6 +127,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } + Attack.DamageMultiplier = 1.0f; float displayRange = Attack.Range; if (damageSource is Item sourceItem) { @@ -192,6 +193,12 @@ namespace Barotrauma item.Condition -= item.MaxCondition * EmpStrength * distFactor; } + var lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f); + } + //discharge batteries var powerContainer = item.GetComponent(); if (powerContainer != null) @@ -264,7 +271,7 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { float distFactor = 1.0f - dist / displayRange; - float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -352,7 +359,7 @@ namespace Barotrauma if (affliction.DivideByLimbCount) { float limbCountFactor = distFactors.Count; - if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index b7f384cea..112f8d5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -51,7 +52,6 @@ namespace Barotrauma //can ambient light get through the gap even if it's not open public bool PassAmbientLight; - //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub private Body outsideCollisionBlocker; @@ -63,8 +63,43 @@ namespace Barotrauma set { if (float.IsNaN(value)) { return; } - if (value > open) { openedTimer = 1.0f; } + if (value > open) + { + openedTimer = 1.0f; + } + if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) + { + if (value > open && value >= 1.0f) + { + InformWaypointsAboutGapState(this, open: true); + } + else if (value < open && open >= 1.0f) + { + InformWaypointsAboutGapState(this, open: false); + } + } open = MathHelper.Clamp(value, 0.0f, 1.0f); + + static void InformWaypointsAboutGapState(Gap gap, bool open) + { + foreach (var wp in WayPoint.WayPointList) + { + if (IsWaypointRightAboveGap(gap, wp)) + { + wp.OnGapStateChanged(open, gap); + } + } + } + + static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp) + { + if (wp.SpawnType != SpawnType.Path) { return false; } + if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; } + if (wp.Position.Y < gap.Rect.Top) { return false; } + if (wp.Position.X > gap.Rect.Right) { return false; } + if (wp.Position.X < gap.Rect.Left) { return false; } + return true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3752320d8..c604cc10d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1083,7 +1083,7 @@ namespace Barotrauma if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open - if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index f48a4ec23..1aeb8ddd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -9,7 +9,7 @@ namespace Barotrauma Vector2 WorldPosition { get; } float Health { get; } - AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index ab5c17c8b..f37d520d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -50,6 +50,12 @@ namespace Barotrauma Description = TextManager.Get($"EntityDescription.{Identifier}"); Tags = Enumerable.Empty().ToImmutableHashSet(); + string description = element.GetAttributeString("description", string.Empty); + if (!description.IsNullOrEmpty()) + { + Description = Description.Fallback(description); + } + List containedItemIDs = new List(); foreach (XElement entityElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 36d95d908..838bddf4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1700,14 +1700,22 @@ namespace Barotrauma foreach (VoronoiCell cell in closeCells) { bool tooClose = false; - foreach (GraphEdge edge in cell.Edges) - { - if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || - Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || - MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + + if (cell.IsPointInsideAABB(position, margin: minDistance)) + { + tooClose = true; + } + else + { + foreach (GraphEdge edge in cell.Edges) { - tooClose = true; - break; + if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || + Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || + MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + { + tooClose = true; + break; + } } } if (tooClose) { tooCloseCells.Add(cell); } @@ -3247,7 +3255,8 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) + if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) || + positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave)) { suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } @@ -3412,8 +3421,7 @@ namespace Barotrauma bool closeEnough = false; foreach (VoronoiCell cell in wall.Cells) { - if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize && - Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize) + if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2)) { closeEnough = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 204bc79b9..090259ff9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -112,10 +112,9 @@ namespace Barotrauma (int)MathUtils.Round(generationParams.Height, Level.GridCellSize)); } - public LevelData(XElement element, float? forceDifficulty = null) + public LevelData(XElement element, float? forceDifficulty = null, bool clampDifficultyToBiome = false) { Seed = element.GetAttributeString("seed", ""); - Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); Size = element.GetAttributePoint("size", new Point(1000)); Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type); @@ -131,10 +130,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\"."); GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type); - if (GenerationParams == null) - { - GenerationParams = LevelGenerationParams.LevelParams.First(); - } + GenerationParams ??= LevelGenerationParams.LevelParams.First(); } InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); @@ -147,6 +143,12 @@ namespace Barotrauma Biome = Biome.Prefabs.First(); } + Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); + if (clampDifficultyToBiome) + { + Difficulty = MathHelper.Clamp(Difficulty, Biome.MinDifficulty, Biome.AdjustedMaxDifficulty); + } + string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 8116071ed..3766dd230 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -67,7 +67,7 @@ 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)] + [Serialize(100.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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 333c19174..6ae55544f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -3,7 +3,6 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -63,10 +62,17 @@ namespace Barotrauma public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; + public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false; + public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; public int LocationTypeChangeCooldown; + /// + /// Is some mission blocking this location from changing its type? + /// + public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public string BaseName { get => baseName; } public string Name { get; private set; } @@ -96,6 +102,7 @@ namespace Barotrauma public class StoreInfo { public Identifier Identifier { get; } + public Identifier MerchantFaction { get; private set; } public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); @@ -105,6 +112,7 @@ namespace Barotrauma /// public int PriceModifier { get; set; } public Location Location { get; } + private float MaxReputationModifier => Location.StoreMaxReputationModifier; private StoreInfo(Location location) { @@ -129,6 +137,7 @@ namespace Barotrauma public StoreInfo(Location location, XElement storeElement) : this(location) { Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), ""); Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute @@ -285,13 +294,14 @@ namespace Barotrauma { price = Location.DailySpecialPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(true); + // Adjust by current reputation + price *= GetReputationModifier(true); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - if (Location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + var faction = GetMerchantOrLocationFactionIdentifier(); + if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); @@ -317,8 +327,8 @@ namespace Barotrauma { price = Location.RequestGoodPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(false); + // Adjust by location reputation + price *= GetReputationModifier(false); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) @@ -331,6 +341,45 @@ namespace Barotrauma return Math.Max((int)price, 1); } + public void SetMerchantFaction(Identifier factionIdentifier) + { + MerchantFaction = factionIdentifier; + } + + public Identifier GetMerchantOrLocationFactionIdentifier() + { + return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty); + } + + public float GetReputationModifier(bool buying) + { + var factionIdentifier = GetMerchantOrLocationFactionIdentifier(); + var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation; + if (reputation == null) { return 1.0f; } + if (buying) + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + else + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + } + public override string ToString() { return Identifier.Value; @@ -392,6 +441,8 @@ namespace Barotrauma } } + + public void SelectMission(Mission mission) { if (!SelectedMissions.Contains(mission) && mission != null) @@ -454,19 +505,22 @@ namespace Barotrauma public bool IsGateBetweenBiomes; - private struct LoadedMission + private readonly struct LoadedMission { - public MissionPrefab MissionPrefab { get; } - public int OriginLocationIndex { get; } - public int DestinationIndex { get; } - public bool SelectedMission { get; } + public readonly MissionPrefab MissionPrefab; + public readonly int TimesAttempted; + public readonly int OriginLocationIndex; + public readonly int DestinationIndex; + public readonly bool SelectedMission; - public LoadedMission(MissionPrefab prefab, int originLocationIndex, int destinationIndex, bool selectedMission) + public LoadedMission(XElement element) { - MissionPrefab = prefab; - OriginLocationIndex = originLocationIndex; - DestinationIndex = destinationIndex; - SelectedMission = selectedMission; + var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty); + MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null; + TimesAttempted = element.GetAttributeInt("timesattempted", 0); + OriginLocationIndex = element.GetAttributeInt("origin", -1); + DestinationIndex = element.GetAttributeInt("destinationindex", -1); + SelectedMission = element.GetAttributeBool("selected", false); } } @@ -577,12 +631,9 @@ namespace Barotrauma killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); - if (Type == null) - { - Type = LocationType.Prefabs.First(); - } + Type ??= LocationType.Prefabs.First(); - LevelData = new LevelData(element.Element("Level")); + LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true); PortraitId = ToolBox.StringToInt(Name); @@ -661,14 +712,11 @@ namespace Barotrauma loadedMissions = new List(); foreach (XElement childElement in missionsElement.GetChildElements("mission")) { - var id = childElement.GetAttributeString("prefabid", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var origin = childElement.GetAttributeInt("origin", -1); - var destination = childElement.GetAttributeInt("destinationindex", -1); - var selected = childElement.GetAttributeBool("selected", false); - loadedMissions.Add(new LoadedMission(prefab, origin, destination, selected)); + var loadedMission = new LoadedMission(childElement); + if (loadedMission.MissionPrefab != null) + { + loadedMissions.Add(loadedMission); + } } } } @@ -693,7 +741,7 @@ namespace Barotrauma Type = newType; Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); - if (Type.HasOutpost) + if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { if (Faction == null) { @@ -734,30 +782,14 @@ namespace Barotrauma { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } - var mission = InstantiateMission(missionPrefab, connection); - if (!mission.Prefab.AllowOtherMissionsInLevel) - { - availableMissions.Clear(); - } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(InstantiateMission(missionPrefab, connection)); } public void UnlockMission(MissionPrefab missionPrefab) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } - var mission = InstantiateMission(missionPrefab); - if (!mission.Prefab.AllowOtherMissionsInLevel) - { - availableMissions.Clear(); - } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(InstantiateMission(missionPrefab)); } public Mission UnlockMissionByIdentifier(Identifier identifier) @@ -778,14 +810,7 @@ namespace Barotrauma { return null; } - if (!mission.Prefab.AllowOtherMissionsInLevel) - { - availableMissions.Clear(); - } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); return mission; } return null; @@ -816,14 +841,7 @@ namespace Barotrauma { return null; } - if (!mission.Prefab.AllowOtherMissionsInLevel) - { - availableMissions.Clear(); - } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); return mission; } else @@ -835,6 +853,20 @@ namespace Barotrauma return null; } + private void AddMission(Mission mission) + { + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#else + (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); +#endif + } + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { if (prefab.IsAllowed(this, this)) @@ -933,6 +965,7 @@ namespace Barotrauma { mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex]; } + mission.TimesAttempted = loadedMission.TimesAttempted; availableMissions.Add(mission); if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } @@ -1332,33 +1365,6 @@ namespace Barotrauma } } - public float GetStoreReputationModifier(bool buying) - { - if (Reputation == null) { return 1.0f; } - 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 static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1488,6 +1494,7 @@ namespace Barotrauma { var storeElement = new XElement("store", new XAttribute("identifier", store.Identifier.Value), + new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction), new XAttribute("balance", store.Balance), new XAttribute("pricemodifier", store.PriceModifier)); foreach (PurchasedItem item in store.Stock) @@ -1532,6 +1539,7 @@ namespace Barotrauma missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), new XAttribute("destinationindex", destinationIndex), + new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted), new XAttribute("origin", originIndex), new XAttribute("selected", selectedMissions.Contains(mission)))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 0db5cb3fd..f2211a472 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -41,6 +41,11 @@ namespace Barotrauma public bool IsEnterable { get; private set; } + /// + /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone + /// + public bool AllowInRandomLevels { get; private set; } + public bool UsePortraitInRandomLoadingScreens { get; @@ -115,6 +120,7 @@ namespace Barotrauma UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); @@ -263,14 +269,31 @@ namespace Barotrauma return names[rand.Next() % names.Length]; } - public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) + public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) { Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); LocationType[] allowedLocationTypes = - Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + Prefabs.Where(lt => + (predicate == null || predicate(lt)) && IsValid(lt)) .OrderBy(p => p.UintIdentifier).ToArray(); + bool IsValid(LocationType lt) + { + if (requireOutpost && !lt.HasOutpost) { return false; } + if (zone.HasValue) + { + if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; } + } + //if zone is not defined, this is a "random" (non-campaign) level + //-> don't choose location types that aren't allowed in those + else if (!lt.AllowInRandomLevels) + { + return false; + } + return true; + } + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 6f6c91070..8d9e03673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; +using static Barotrauma.LocationTypeChange; namespace Barotrauma { @@ -58,7 +58,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); - RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); @@ -91,37 +91,30 @@ namespace Barotrauma } } + public bool AnyWithinDistance(Location startLocation, int distance) + { + return Map.LocationOrConnectionWithinDistance( + startLocation, + maxDistance: distance, + criteria: MatchesLocation, + connectionCriteria: MatchesConnection); + } + public bool MatchesLocation(Location location) { return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); } - public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + public bool MatchesConnection(LocationConnection connection) { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && MatchesLocation(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) { - if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) - { - return true; - } - if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) - { - return true; - } - - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } - } + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; } - return false; } } @@ -227,8 +220,9 @@ namespace Barotrauma if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } if (location.IsGateBetweenBiomes) { return 0.0f; } - if (DisallowedAdjacentLocations.Any() && - AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + if (DisallowedAdjacentLocations.Any() && + Map.LocationOrConnectionWithinDistance(location, DisallowedProximity, + (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) { return 0.0f; } @@ -247,7 +241,6 @@ namespace Barotrauma probability *= requirement.Probability; } } - if (location.ProximityTimer.ContainsKey(requirement)) { if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) @@ -266,25 +259,5 @@ namespace Barotrauma return probability; } - - private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) - { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && predicate(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) - { - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } - } - } - - return false; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index d2d93d4c0..494653ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -70,13 +70,13 @@ namespace Barotrauma public List Locations { get; private set; } private readonly List locationsDiscovered = new List(); - private readonly List outpostsVisited = new List(); + private readonly List locationsVisited = new List(); public List Connections { get; private set; } public Radiation Radiation; - private bool wasLocationDiscoveryOrderTracked = true; + private bool trackedLocationDiscoveryAndVisitOrder = true; public Map(CampaignSettings settings) { @@ -939,6 +939,12 @@ namespace Barotrauma public void MoveToNextLocation() { + if (SelectedLocation == null && Level.Loaded?.EndLocation != null) + { + //force the location at the end of the level to be selected, even if it's been deselect on the map + //(e.g. due to returning to an empty location the beginning of the level during the round) + SelectLocation(Level.Loaded.EndLocation); + } if (SelectedConnection == null) { if (!endLocations.Contains(CurrentLocation)) @@ -1043,12 +1049,24 @@ namespace Barotrauma Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); - SelectedConnection = - Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? - Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (currentDisplayLocation == SelectedLocation) + { + SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } + else + { + SelectedConnection = + Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? + 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()); + string errorMsg = + $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); } if (prevSelected != SelectedLocation) { @@ -1266,51 +1284,6 @@ namespace Barotrauma return false; } - public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) - { - if (startingLocation.Type.HasOutpost) - { - endingLocation = startingLocation; - return 0; - } - - int iterations = 0; - int distance = 0; - endingLocation = null; - - List testedLocations = new List(); - List locationsToTest = new List { startingLocation }; - - while (endingLocation == null && iterations < 100) - { - List nextTestingBatch = new List(); - for (int i = 0; i < locationsToTest.Count; i++) - { - Location testLocation = locationsToTest[i]; - for (int j = 0; j < testLocation.Connections.Count; j++) - { - Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation); - if (potentialOutpost.Type.HasOutpost) - { - distance = iterations + 1; - endingLocation = potentialOutpost; - } - else if (!testedLocations.Contains(potentialOutpost)) - { - nextTestingBatch.Add(potentialOutpost); - } - } - - testedLocations.Add(testLocation); - } - - locationsToTest = nextTestingBatch; - iterations++; - } - - return distance; - } - private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { string prevName = location.Name; @@ -1321,6 +1294,8 @@ namespace Barotrauma return false; } + if (location.LocationTypeChangesBlocked) { return false; } + if (newType.OutpostTeam != location.Type.OutpostTeam || newType.HasOutpost != location.Type.HasOutpost) { @@ -1338,6 +1313,50 @@ namespace Barotrauma return true; } + public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance; + } + + /// + /// Get the shortest distance from the start location to another location that satisfies the specified criteria. + /// + /// The distance to a matching location, or int.MaxValue if none are found. + public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + int distance = 0; + var locationsToTest = new List() { startLocation }; + var nextBatchToTest = new HashSet(); + var checkedLocations = new HashSet(); + while (locationsToTest.Any()) + { + foreach (var location in locationsToTest) + { + checkedLocations.Add(location); + if (criteria(location)) { return distance; } + foreach (var connection in location.Connections) + { + if (connectionCriteria != null && connectionCriteria(connection)) + { + return distance; + } + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + nextBatchToTest.Add(otherLocation); + } + } + if (distance > maxDistance) { return int.MaxValue; } + } + distance++; + locationsToTest.Clear(); + locationsToTest.AddRange(nextBatchToTest); + nextBatchToTest.Clear(); + } + return int.MaxValue; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1356,29 +1375,38 @@ namespace Barotrauma public void Visit(Location location) { if (location is null) { return; } - if (!location.HasOutpost()) { return; } - if (outpostsVisited.Contains(location)) { return; } - outpostsVisited.Add(location); + if (locationsVisited.Contains(location)) { return; } + locationsVisited.Add(location); + RemoveFogOfWarProjSpecific(location); } public void ClearLocationHistory() { locationsDiscovered.Clear(); - outpostsVisited.Clear(); + locationsVisited.Clear(); } public int? GetDiscoveryIndex(Location location) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } return locationsDiscovered.IndexOf(location); } - public int? GetVisitIndex(Location location) + public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } - return outpostsVisited.IndexOf(location); + int index = locationsVisited.IndexOf(location); + if (includeLocationsWithoutOutpost) { return index; } + int noOutpostLocations = 0; + for (int i = 0; i < index; i++) + { + if (locationsVisited[i] is not Location l) { continue; } + if (l.HasOutpost()) { continue; } + noOutpostLocations++; + } + return index - noOutpostLocations; } public bool IsDiscovered(Location location) @@ -1387,6 +1415,14 @@ namespace Barotrauma return locationsDiscovered.Contains(location); } + public bool IsVisited(Location location) + { + if (location is null) { return false; } + return locationsVisited.Contains(location); + } + + partial void RemoveFogOfWarProjSpecific(Location location); + /// /// Load a previously saved map from an xml element /// @@ -1431,11 +1467,13 @@ namespace Barotrauma } location.LoadLocationTypeChange(subElement); - // Backwards compatibility + // Backwards compatibility: if the discovery status is defined in the location element, + // the game was saved using when the discovery order still wasn't being tracked if (subElement.GetAttributeBool("discovered", false)) { Discover(location); - wasLocationDiscoveryOrderTracked = false; + Visit(location); + trackedLocationDiscoveryAndVisitOrder = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); @@ -1472,32 +1510,45 @@ namespace Barotrauma Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; case "discovered": + bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false); foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Discover(l); + if (GetLocation(childElement) is Location l) + { + Discover(l); + if (!trackedVisitedEmptyLocations) + { + if (!l.HasOutpost()) + { + Visit(l); + } + trackedLocationDiscoveryAndVisitOrder = false; + } + } } break; case "visited": foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Visit(l); + if (GetLocation(childElement) is Location l) + { + Visit(l); + } } break; } + + Location GetLocation(XElement element) + { + int index = element.GetAttributeInt("i", -1); + if (index < 0) { return null; } + return Locations[index]; + } } void Discover(Location location) { this.Discover(location, checkTalents: false); -#if CLIENT - RemoveFogOfWar(location); -#endif if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; @@ -1509,9 +1560,6 @@ namespace Barotrauma location?.InstantiateLoadedMissions(this); } -#if RELEASE - TODO: MAKE SURE THE VERSION NUMBER BELOW IS CORRECT FOR THE FULL RELEASE (OR WHICHEVER UPDATE WE ADD THE FACTIONS IN) -#endif //backwards compatibility: //if the save is from a version prior to the addition of faction-specific outposts, assign factions if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null)) @@ -1599,7 +1647,8 @@ namespace Barotrauma if (locationsDiscovered.Any()) { - var discoveryElement = new XElement("discovered"); + var discoveryElement = new XElement("discovered", + new XAttribute("trackedvisitedemptylocations", true)); foreach (Location location in locationsDiscovered) { int index = Locations.IndexOf(location); @@ -1609,10 +1658,10 @@ namespace Barotrauma mapElement.Add(discoveryElement); } - if (outpostsVisited.Any()) + if (locationsVisited.Any()) { var visitElement = new XElement("visited"); - foreach (Location location in outpostsVisited) + foreach (Location location in locationsVisited) { int index = Locations.IndexOf(location); var locationElement = new XElement("location", new XAttribute("i", index)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 443c45743..7b680d358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -59,7 +59,24 @@ namespace Barotrauma //the position and dimensions of the entity protected Rectangle rect; - public bool ExternalHighlight = false; + protected static readonly HashSet highlightedEntities = new HashSet(); + + public static IEnumerable HighlightedEntities => highlightedEntities; + + + private bool externalHighlight = false; + public bool ExternalHighlight + { + get { return externalHighlight; } + set + { + if (value != externalHighlight) + { + externalHighlight = value; + CheckIsHighlighted(); + } + } + } //is the mouse inside the rect private bool isHighlighted; @@ -67,7 +84,14 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set { isHighlighted = value; } + set + { + if (value != IsHighlighted) + { + isHighlighted = value; + CheckIsHighlighted(); + } + } } public virtual Rectangle Rect @@ -362,6 +386,30 @@ namespace Barotrauma return true; } + protected virtual void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + + private static readonly List tempHighlightedEntities = new List(); + public static void ClearHighlightedEntities() + { + tempHighlightedEntities.Clear(); + tempHighlightedEntities.AddRange(highlightedEntities); + foreach (var entity in tempHighlightedEntities) + { + entity.IsHighlighted = false; + } + } + + public abstract MapEntity Clone(); public static List Clone(List entitiesToClone) @@ -649,6 +697,9 @@ namespace Barotrauma List entities = new List(); foreach (var element in parentElement.Elements()) { +#if CLIENT + GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested(); +#endif string typeName = element.Name.ToString(); Type t; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 6d9077cc0..2c9624004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -174,6 +174,9 @@ namespace Barotrauma public abstract Sprite Sprite { get; } + public virtual bool CanSpriteFlipX { get; } = false; + public virtual bool CanSpriteFlipY { get; } = false; + public abstract string OriginalName { get; } public abstract LocalizedString Name { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index e127601df..6b95736f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -150,10 +150,13 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags = - onlyEntrance ? - (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : - SelectModules(outpostModules, location, generationParams); + List pendingModuleFlags = new List(); + if (generationParams.ModuleCounts.Any()) + { + pendingModuleFlags = onlyEntrance ? + generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, location, generationParams); + } foreach (Identifier flag in pendingModuleFlags) { @@ -1446,6 +1449,16 @@ namespace Barotrauma { me.HiddenInGame = location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index f55c7f6a0..97a7eec42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -20,8 +20,8 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; - public readonly bool CanSpriteFlipX; - public readonly bool CanSpriteFlipY; + public override bool CanSpriteFlipX { get; } + public override bool CanSpriteFlipY { get; } /// /// If null, the orientation is determined automatically based on the dimensions of the structure instances diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index da55e612d..4d17081eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1379,19 +1379,6 @@ namespace Barotrauma 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) - { - HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; - } - - foreach (Submarine sub in loaded) - { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); - } - IdOffset = IdRemap.DetermineNewOffset(); List newEntities = new List(); @@ -1419,7 +1406,6 @@ namespace Barotrauma 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); @@ -1441,6 +1427,17 @@ namespace Barotrauma } subBody = new SubmarineBody(this, showErrorMessages); + + //place the sub above the top of the level + HiddenSubPosition = HiddenSubStartPosition; + if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + { + HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; + } + foreach (Submarine sub in loaded) + { + HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + } Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); @@ -1745,57 +1742,66 @@ namespace Barotrauma public static void Unload() { + if (Unloading) + { + DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading."); + return; + } + Unloading = true; + try + { #if CLIENT - RoundSound.RemoveAllRoundSounds(); - GameMain.LightManager?.ClearLights(); + RoundSound.RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif - var _loaded = new List(loaded); - foreach (Submarine sub in _loaded) - { - sub.Remove(); - } - - loaded.Clear(); - - visibleEntities = null; - - if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } - - RemoveAll(); - - if (Item.ItemList.Count > 0) - { - List items = new List(Item.ItemList); - foreach (Item item in items) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { - DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); - try - { - item.Remove(); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); - } + sub.Remove(); } - Item.ItemList.Clear(); + + loaded.Clear(); + + visibleEntities = null; + + if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } + + RemoveAll(); + + if (Item.ItemList.Count > 0) + { + List items = new List(Item.ItemList); + foreach (Item item in items) + { + DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); + try + { + item.Remove(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); + } + } + Item.ItemList.Clear(); + } + + Ragdoll.RemoveAll(); + PhysicsBody.RemoveAll(); + GameMain.World = null; + + Powered.Grids.Clear(); + + GC.Collect(); + + } + finally + { + Unloading = false; } - - Ragdoll.RemoveAll(); - - PhysicsBody.RemoveAll(); - - GameMain.World?.Clear(); - GameMain.World = null; - - Powered.Grids.Clear(); - - GC.Collect(); - - Unloading = false; } public override void Remove() @@ -1862,18 +1868,18 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed) { continue; } + if (connectedWp.IsObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition); var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; break; } } @@ -1892,11 +1898,11 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; } + if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); @@ -1904,8 +1910,8 @@ namespace Barotrauma { if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { nodes = new HashSet(); @@ -1927,7 +1933,7 @@ namespace Barotrauma { if (obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { - nodes.ForEach(n => n.Waypoint.isObstructed = false); + nodes.ForEach(n => n.Waypoint.IsObstructed = false); nodes.Clear(); obstructedNodes.Remove(otherSub); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 9b7b4d176..e498b3aa0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -46,6 +46,7 @@ namespace Barotrauma } private float depthDamageTimer = 10.0f; + private float damageSoundTimer = 10.0f; private readonly Submarine submarine; @@ -507,36 +508,58 @@ namespace Barotrauma if (Level.Loaded == null) { return; } //camera shake and sounds start playing 500 meters before crush depth - float depthEffectThreshold = 500.0f; - if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold) + const float CosmeticEffectThreshold = -500.0f; + //breaches won't get any more severe 500 meters below crush depth + const float MaxEffectThreshold = 500.0f; + const float MinWallDamageProbability = 0.1f; + const float MaxWallDamageProbability = 1.0f; + const float MinWallDamage = 50f; + const float MaxWallDamage = 500.0f; + const float MinCameraShake = 5f; + const float MaxCameraShake = 50.0f; + + if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold) { return; } - depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) { return; } - -#if CLIENT - SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); -#endif - - foreach (Structure wall in Structure.WallList) + damageSoundTimer -= deltaTime; + if (damageSoundTimer <= 0.0f) { - if (wall.Submarine != submarine) { continue; } - - float wallCrushDepth = wall.CrushDepth; - float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; - if (pastCrushDepth > 0) - { - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); - } - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) - { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f)); - } +#if CLIENT + SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); +#endif + damageSoundTimer = Rand.Range(5.0f, 10.0f); } - depthDamageTimer = 10.0f; + depthDamageTimer -= deltaTime; + if (depthDamageTimer <= 0.0f) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != submarine) { continue; } + + float wallCrushDepth = wall.CrushDepth; + float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; + float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f); + + if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; } + + float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio); + if (pastCrushDepth > 0) + { + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f); +#if CLIENT + SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f); +#endif + } + if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + { + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio)); + } + } + depthDamageTimer = Rand.Range(5.0f, 10.0f); + } } public void FlipX() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index b1885e71b..356ee4dcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -479,7 +479,6 @@ namespace Barotrauma hashTask = new Task(() => { hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); - Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -532,11 +531,15 @@ namespace Barotrauma /// /// Calculated from . Can be used when the sub hasn't been loaded and we can't access . /// - public float GetRealWorldCrushDepth() + public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth) { - if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; } + if (SubmarineElement == null) + { + realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; + return false; + } bool structureCrushDepthsDefined = false; - float realWorldCrushDepth = float.PositiveInfinity; + realWorldCrushDepth = float.PositiveInfinity; foreach (var structureElement in SubmarineElement.GetChildElements("structure")) { string name = structureElement.Attribute("name")?.Value ?? ""; @@ -554,7 +557,7 @@ namespace Barotrauma { realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; } - return realWorldCrushDepth; + return structureCrushDepthsDefined; } public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag) { @@ -595,7 +598,6 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -776,7 +778,7 @@ namespace Barotrauma float price = Price; - if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive) + if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 60a68357b..2bb863cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -29,7 +29,32 @@ namespace Barotrauma private HashSet tags; - public bool isObstructed; + public bool IsObstructed; + + public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y; + + // Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open. + // The only way to add the open gaps should be by calling OnGapStateSchanged. + public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater); + + private HashSet openGaps; + /// + /// Only called by a Gap when the state changes. + /// So in practice used like an event callback, although technically just a method + /// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle) + /// + public void OnGapStateChanged(bool open, Gap gap) + { + openGaps ??= new HashSet(); + if (open) + { + openGaps.Add(gap); + } + else + { + openGaps.Remove(gap); + } + } private ushort gapId; public Gap ConnectedGap @@ -989,6 +1014,12 @@ namespace Barotrauma public override void OnMapLoaded() { + if (Submarine == null) + { + // Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc. + // Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost. + return; + } InitializeLinks(); FindHull(); FindStairs(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index e08ff3ca1..dbb4c9276 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Networking public readonly Either AddressOrAccountId; public readonly string Reason; - public DateTime? ExpirationTime; + public Option ExpirationTime; public readonly UInt32 UniqueIdentifier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 93a7b09eb..d453464f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Networking public T GetVote(VoteType voteType) { - return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T); + return (votes[(int)voteType] is T t) ? t : default; } public void SetVote(VoteType voteType, object value) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index e3f946325..cba0112e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -160,7 +160,8 @@ namespace Barotrauma { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteIdentifier) }, { typeof(AccountId), new ReadWriteBehavior(ReadAccountId, WriteAccountId) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, - { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } + { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }, + { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) } }; private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> @@ -347,12 +348,7 @@ namespace Barotrauma } private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : struct => - ReadOption(inc, attribute, bitField) switch - { - Some { Value: var value } => value, - None _ => null, - _ => throw new ArgumentOutOfRangeException() - }; + ReadOption(inc, attribute, bitField).TryUnwrap(out var value) ? value : null; private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : struct => WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); @@ -377,7 +373,7 @@ namespace Barotrauma { ToolBox.ThrowIfNull(option); - if (option.TryUnwrap(out T value)) + if (option.TryUnwrap(out T? value)) { bitField.WriteBoolean(true); if (TryFindBehavior(out ReadWriteBehavior behavior)) @@ -512,6 +508,41 @@ namespace Barotrauma WriteSingle(y, attribute, msg, bitField); } + private static readonly Range ValidTickRange + = new Range( + start: DateTime.MinValue.Ticks, + end: DateTime.MaxValue.Ticks); + private static readonly Range ValidTimeZoneMinuteRange + = new Range( + start: (Int16)TimeSpan.FromHours(-12).TotalMinutes, + end: (Int16)TimeSpan.FromHours(14).TotalMinutes); + + private static SerializableDateTime ReadSerializableDateTime( + IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) + { + var ticks = inc.ReadInt64(); + var timezone = inc.ReadInt16(); + + if (!ValidTickRange.Contains(ticks)) + { + throw new Exception($"Incoming SerializableDateTime ticks out of range (ticks: {ticks}, timezone: {timezone})"); + } + if (!ValidTimeZoneMinuteRange.Contains(timezone)) + { + throw new Exception($"Incoming SerializableDateTime timezone out of range (ticks: {ticks}, timezone: {timezone})"); + } + + return new SerializableDateTime(new DateTime(ticks), + new SerializableTimeZone(TimeSpan.FromMinutes(timezone))); + } + + private static void WriteSerializableDateTime( + SerializableDateTime dateTime, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) + { + msg.WriteInt64(dateTime.Ticks); + msg.WriteInt16((Int16)(dateTime.TimeZone.Value.Ticks / TimeSpan.TicksPerMinute)); + } + private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue; private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index d4b897c9c..0f33d9599 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking /// public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { @@ -110,7 +110,7 @@ namespace Barotrauma.Networking WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } - public struct OrderMessageInfo + public readonly struct OrderMessageInfo { public Identifier OrderIdentifier { get; } public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 02fc9f386..c8042a945 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -302,7 +302,7 @@ namespace Barotrauma.Networking public ServerContentPackage() { } - public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime) + public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime) { Name = contentPackage.Name; Hash = contentPackage.Hash; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index e4cbc1bb7..cdeaa24de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -111,27 +111,24 @@ namespace Barotrauma.Networking switch (typeString) { case "float": - if (!(a is float?)) return false; - if (!(b is float?)) return false; - return MathUtils.NearlyEqual((float)a, (float)b); + if (a is not float fa) { return false; } + if (b is not float fb) { return false; } + return MathUtils.NearlyEqual(fa, fb); case "int": - if (!(a is int?)) return false; - if (!(b is int?)) return false; - return (int)a == (int)b; + if (a is not int ia) { return false; } + if (b is not int ib) { return false; } + return ia == ib; case "bool": - if (!(a is bool?)) return false; - if (!(b is bool?)) return false; - return (bool)a == (bool)b; + if (a is not bool ba) { return false; } + if (b is not bool bb) { return false; } + return ba == bb; case "Enum": - if (!(a is Enum)) return false; - if (!(b is Enum)) return false; - return ((Enum)a).Equals((Enum)b); + if (a is not Enum ea) { return false; } + if (b is not Enum eb) { return false; } + return ea.Equals(eb); default: - if (a == null || b == null) - { - return (a == null) == (b == null); - } - return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); + return ReferenceEquals(a,b) + || string.Equals(a?.ToString(), b?.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -204,7 +201,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) { overrideValue = Value; } + overrideValue ??= Value; switch (typeString) { case "float": @@ -293,10 +290,7 @@ namespace Barotrauma.Networking var saveProperties = SerializableProperty.GetProperties(this); foreach (var property in saveProperties) { - object value = property.GetValue(this); - if (value == null) { continue; } - - string typeName = SerializableProperty.GetSupportedTypeName(value.GetType()); + string typeName = SerializableProperty.GetSupportedTypeName(property.PropertyType); if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); @@ -754,6 +748,9 @@ namespace Barotrauma.Networking get; set; } + + [Serialize(defaultValue: "", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] @@ -1093,7 +1090,7 @@ namespace Barotrauma.Networking for (int i = 0; i < count; i++) { int index = msg.ReadUInt16(); - if (index < 0 || index >= subList.Count) { continue; } + if (index >= subList.Count) { continue; } string submarineName = subList[index].Name; HiddenSubs.Add(submarineName); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 594171239..14ae70d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; - private IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) + private static IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) { Dictionary voteList = new Dictionary(); @@ -29,7 +29,7 @@ namespace Barotrauma return voteList; } - public T HighestVoted(VoteType voteType, List voters) + public static T HighestVoted(VoteType voteType, IEnumerable voters) { if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index e5bf41bca..aa877db24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,6 +12,8 @@ namespace Barotrauma { public Identifier VariantOf { get; } + public T? ParentPrefab { get; set; } + public void InheritFrom(T parent); } @@ -20,8 +24,10 @@ namespace Barotrauma #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); } - - public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + + public delegate void VariantXMLChecker(XElement originalElement, XElement? variantElement, XElement result); + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement, VariantXMLChecker? checker = null) { XElement newElement = new XElement(variantElement.Name); newElement.Add(baseElement.Attributes()); @@ -31,6 +37,9 @@ namespace Barotrauma void ReplaceElement(XElement element, XElement replacement) { + XElement originalElement = new XElement(element); + + List newElementsFromBase = new List(element.Elements()); List elementsToRemove = new List(); foreach (XAttribute attribute in replacement.Attributes()) { @@ -48,6 +57,7 @@ namespace Barotrauma if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) { matchingElementFound = true; + newElementsFromBase.Clear(); elementsToRemove.AddRange(element.Elements()); break; } @@ -65,6 +75,7 @@ namespace Barotrauma ReplaceElement(subElement, replacementSubElement); } matchingElementFound = true; + newElementsFromBase.Remove(subElement); break; } i++; @@ -75,11 +86,16 @@ namespace Barotrauma } } elementsToRemove.ForEach(e => e.Remove()); + checker?.Invoke(originalElement, replacement, element); + foreach (XElement newElement in newElementsFromBase) + { + checker?.Invoke(newElement, null, newElement); + } } void ReplaceAttribute(XElement element, XAttribute newAttribute) { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + XAttribute? existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); if (existingAttribute == null) { element.Add(newAttribute); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 4b3b7edc9..e11ea36db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -125,7 +125,7 @@ namespace Barotrauma public Node? AddNodeAndInheritors(Identifier id) { - if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + if (!prefabCollection.TryGet(id, out T? _, requireInheritanceValid: false)) { return null; } if (!IdToNode.TryGetValue(id, out var node)) { @@ -139,24 +139,25 @@ namespace Barotrauma //all inheritors so let's just return this immediately return node; } - - prefabCollection - .Cast>() - .Where(p => p.VariantOf == id) - .Cast() - .ForEach(p => - { - var inheritorNode = AddNodeAndInheritors(p.Identifier); - if (inheritorNode is null) { return; } - RootNodes.Remove(inheritorNode); - inheritorNode.Parent = node; - node.Inheritors.Add(inheritorNode); - }); + var enumerator = prefabCollection.GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is not IImplementsVariants implementsVariants || implementsVariants.VariantOf != id) + { + continue; + } + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { continue; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + } return node; } - private void FindCycles(in Node node, HashSet uncheckedNodes) + private static void FindCycles(in Node node, HashSet uncheckedNodes) { HashSet checkedNodes = new HashSet(); List hierarchyPositions = new List(); @@ -183,24 +184,45 @@ namespace Barotrauma public void InvokeCallbacks() { HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); - IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + IdToNode.Values.ForEach(v => PrefabCollection.InheritanceTreeCollection.FindCycles(v, uncheckedNodes)); void invokeCallbacksForNode(Node node) { - if (!prefabCollection.TryGet(node.Identifier, out var p) || - !(p is IImplementsVariants prefab)) { return; } - if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + if (!prefabCollection.TryGet(node.Identifier, out var p, requireInheritanceValid: false) || + p is not IImplementsVariants prefab) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent, requireInheritanceValid: false)) + { + prefab.InheritFrom(parent); + prefab.ParentPrefab = parent; + } node.Inheritors.ForEach(invokeCallbacksForNode); } RootNodes.ForEach(invokeCallbacksForNode); } } + private static bool IsInheritanceValid(T? prefab) + { + if (prefab == null) { return false; } + return + prefab is not IImplementsVariants implementsVariants || + (implementsVariants.VariantOf.IsEmpty || (implementsVariants.ParentPrefab != null && IsInheritanceValid(implementsVariants.ParentPrefab))); + } + private void HandleInheritance(Identifier prefabIdentifier) => HandleInheritance(prefabIdentifier.ToEnumerable()); private void HandleInheritance(IEnumerable identifiers) { if (!implementsVariants) { return; } + foreach (var id in identifiers) + { + if (!TryGet(id, out T? prefab, requireInheritanceValid: false)) { continue; } + if (prefab is IImplementsVariants implementsVariants && !implementsVariants.VariantOf.IsEmpty) + { + //reset parent prefab, it'll get set in InvokeCallbacks if the inheritance is valid + implementsVariants.ParentPrefab = null; + } + } InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); inheritanceTreeCollection.AddNodesAndInheritors(identifiers); inheritanceTreeCollection.InvokeCallbacks(); @@ -213,9 +235,11 @@ namespace Barotrauma { get { - foreach (var prefab in prefabs) + foreach (var kvp in prefabs) { - yield return prefab; + var prefab = kvp.Value.ActivePrefab; + if (!IsInheritanceValid(prefab)) { continue; } + yield return kvp; } } } @@ -231,7 +255,8 @@ namespace Barotrauma { Prefab.DisallowCallFromConstructor(); var prefab = prefabs[identifier].ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + if (prefab != null && !IsPrefabOverriddenByFile(prefab) && + IsInheritanceValid(prefab)) { return prefab; } @@ -258,12 +283,17 @@ namespace Barotrauma /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) + { + return TryGet(identifier, out result, requireInheritanceValid: true); + } + + private bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result, bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector) && selector.ActivePrefab != null) { result = selector!.ActivePrefab; - return true; + return !requireInheritanceValid || IsInheritanceValid(result); } else { @@ -304,7 +334,7 @@ namespace Barotrauma public bool ContainsKey(Identifier identifier) { Prefab.DisallowCallFromConstructor(); - return prefabs.ContainsKey(identifier); + return TryGet(identifier, out _); } public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); @@ -460,6 +490,19 @@ namespace Barotrauma topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; OnSort?.Invoke(); HandleInheritance(this.Select(p => p.Identifier)); + + var enumerator = GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is IImplementsVariants implementsVariants && !IsInheritanceValid(p)) + { + DebugConsole.ThrowError( + $"Error in content package \"{p.ContentFile.ContentPackage.Name}\": " + + $"could not find the prefab \"{implementsVariants.VariantOf}\" the prefab \"{p.Identifier}\" is configured as a variant of."); + continue; + } + } } /// @@ -467,15 +510,19 @@ namespace Barotrauma /// /// IEnumerator public IEnumerator GetEnumerator() + { + return GetEnumerator(requireInheritanceValid: true); + } + + private IEnumerator GetEnumerator(bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - foreach (var kpv in prefabs) + foreach (var kvp in prefabs) { - var prefab = kpv.Value.ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) - { - yield return prefab; - } + var prefab = kvp.Value.ActivePrefab; + if (prefab == null || IsPrefabOverriddenByFile(prefab)) { continue; } + if (requireInheritanceValid && !IsInheritanceValid(prefab)) { continue; } + yield return prefab; } } @@ -485,7 +532,7 @@ namespace Barotrauma /// IEnumerator IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + return GetEnumerator(requireInheritanceValid: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 62b8c8056..557860b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -201,17 +201,28 @@ namespace Voronoi2 public bool IsPointInside(Vector2 point) { + if (!IsPointInsideAABB(point, margin: 0.0f)) { return false; } Vector2 transformedPoint = point - Translation; - if (Edges.All(e => e.Point1.X < transformedPoint.X && e.Point2.X < transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y < transformedPoint.Y && e.Point2.Y < transformedPoint.Y)) { return false; } - if (Edges.All(e => e.Point1.X > transformedPoint.X && e.Point2.X > transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y > transformedPoint.Y && e.Point2.Y > transformedPoint.Y)) { return false; } foreach (GraphEdge edge in Edges) { if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } + + public bool IsPointInsideAABB(Vector2 point2, float margin) + { + Vector2 transformedPoint = point2 - Translation; + Vector2 max = transformedPoint + Vector2.One * margin; + Vector2 min = transformedPoint - Vector2.One * margin; + + if (Edges.All(e => e.Point1.X < min.X && e.Point2.X < min.X)) { return false; } + if (Edges.All(e => e.Point1.Y < min.Y && e.Point2.Y < min.Y)) { return false; } + if (Edges.All(e => e.Point1.X > max.X && e.Point2.X > max.X)) { return false; } + if (Edges.All(e => e.Point1.Y > max.Y && e.Point2.Y > max.Y)) { return false; } + + return true; + } } public class GraphEdge diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 7b97635be..9b8bc37d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -61,10 +61,7 @@ namespace Barotrauma GameMain.GameSession?.CrewManager?.AutoShowCrewList(); #endif - foreach (MapEntity entity in MapEntity.mapEntityList) - { - entity.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if RUN_PHYSICS_IN_SEPARATE_THREAD var physicsThread = new Thread(ExecutePhysics) @@ -140,10 +137,7 @@ namespace Barotrauma { if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } } - foreach (MapEntity e in MapEntity.mapEntityList) - { - e.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index c78758bf3..5e5317022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -154,6 +154,7 @@ namespace Barotrauma { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Identifier), "identifier" }, + { typeof(LanguageIdentifier), "languageidentifier" }, { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, @@ -240,7 +241,7 @@ namespace Barotrauma switch (typeName) { case "bool": - bool boolValue = value == "true" || value == "True"; + bool boolValue = value.ToIdentifier() == "true"; if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; @@ -290,6 +291,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, value.ToIdentifier()); break; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, value.ToLanguageIdentifier()); + break; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString(value)); break; @@ -373,6 +377,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, new Identifier((string)value)); return true; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, ((string)value).ToLanguageIdentifier()); + return true; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString((string)value)); return true; @@ -556,7 +563,7 @@ namespace Barotrauma public static string GetSupportedTypeName(Type type) { - if (type.IsEnum) return "Enum"; + if (type.IsEnum) { return "Enum"; } if (!supportedTypes.TryGetValue(type, out string typeName)) { return null; @@ -693,6 +700,29 @@ namespace Barotrauma case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { value = character.PropulsionSpeedMultiplier; return true; } } + break; + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } + break; + case nameof(Character.HullOxygenPercentage): + { + if (parentObject is Character character) + { + value = character.HullOxygenPercentage; + return true; + } + else if (parentObject is Item item) + { + value = item.HullOxygenPercentage; + return true; + } + } + break; + case nameof(Door.Stuck): + { if (parentObject is Door door) { value = door.Stuck; return true; } } + break; } return false; } @@ -740,6 +770,23 @@ namespace Barotrauma case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; + case nameof(Character.InWater): + { + if (parentObject is Character character) + { + value = character.InWater; + return true; + } + else if (parentObject is Item item) + { + value = item.InWater; + return true; + } + } + break; + case nameof(Rope.Snapped): + if (parentObject is Rope rope) { value = rope.Snapped; return true; } + break; } return false; } @@ -769,7 +816,7 @@ namespace Barotrauma switch (Name) { case nameof(Item.Condition): - if (parentObject is Item item) { item.Condition = value; return true; } + { if (parentObject is Item item) { item.Condition = value; return true; } } break; case nameof(Powered.Voltage): if (parentObject is Powered powered) { powered.Voltage = value; return true; } @@ -801,6 +848,9 @@ namespace Barotrauma case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; + case nameof(Item.Scale): + { if (parentObject is Item item) { item.Scale = value; return true; } } + break; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 33f28c356..fa9610d09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,15 +1,14 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Xml; using System.Xml.Linq; -using Microsoft.Xna.Framework; using File = Barotrauma.IO.File; using FileStream = Barotrauma.IO.FileStream; using Path = Barotrauma.IO.Path; @@ -315,7 +314,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -357,7 +356,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -376,7 +375,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -395,12 +394,22 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; } + public static Option GetAttributeDateTime( + this XElement element, string name) + { + var attribute = element?.GetAttribute(name); + if (attribute == null) { return Option.None(); } + + string attrVal = attribute.Value; + return SerializableDateTime.Parse(attrVal); + } + public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) { var attribute = element?.GetAttribute(name); @@ -414,7 +423,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -439,7 +448,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -464,7 +473,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -475,9 +484,18 @@ namespace Barotrauma { var attr = element?.GetAttribute(name); if (attr == null) { return defaultValue; } - return Enum.TryParse(attr.Value, true, out T result) ? result : - int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt) ? Unsafe.As(ref resultInt) : - defaultValue; + + if (Enum.TryParse(attr.Value, true, out T result)) + { + return result; + } + else if (int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt)) + { + return Unsafe.As(ref resultInt); + } + DebugConsole.ThrowError($"Error in {attr}! \"{attr}\" is not a valid {typeof(T).Name} value"); + return default; + } public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) @@ -566,13 +584,26 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } return colorValue; } + private static void LogAttributeError(XAttribute attribute, XElement element, Exception e) + { + string elementStr = element.ToString(); + if (elementStr.Length > 500) + { + DebugConsole.ThrowError($"Error when reading attribute \"{attribute}\"!", e); + } + else + { + DebugConsole.ThrowError($"Error when reading attribute \"{attribute.Name}\" from {elementStr}!", e); + } + } + #if CLIENT public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue) { @@ -586,10 +617,18 @@ namespace Barotrauma return mouseButton; } else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && - (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + Enum.GetValues().Contains((MouseButton)mouseButtonInt)) { return (MouseButton)mouseButtonInt; } + else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse; + } + else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse; + } return defaultValue; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs index 5886c8ba3..617058963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -1,13 +1,134 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.IO; +using XmlWriterSettings = System.Xml.XmlWriterSettings; +#nullable enable namespace Barotrauma { - public class CreatureMetrics + public static class CreatureMetrics { - public readonly HashSet RecentlyEncountered = new HashSet(); - public readonly HashSet Encountered = new HashSet(); - public readonly HashSet Killed = new HashSet(); + private const string path = "creature_metrics.xml"; - public readonly static CreatureMetrics Instance = new CreatureMetrics(); + /// + /// Resets every round. + /// + public static HashSet RecentlyEncountered { get; private set; } = new HashSet(); + public static HashSet Encountered { get; private set; } = new HashSet(); + public static HashSet Unlocked { get; private set; } = new HashSet(); + public static HashSet Killed { get; private set; } = new HashSet(); + public static bool IsInitialized { get; private set; } + public static bool UnlockAll { get; set; } + + public static void Init() + { + IsInitialized = true; + if (File.Exists(path)) + { + Load(); + } + Save(); + } + + private static void Load() + { + XDocument doc = XMLExtensions.TryLoadXml(path); + XElement? root = doc?.Root; + if (root == null) + { + DebugConsole.AddWarning($"Failed to load creature metrics from {path}!"); + return; + } + UnlockAll = root.GetAttributeBool(nameof(UnlockAll), UnlockAll); + Unlocked = new HashSet(root.GetAttributeIdentifierArray(nameof(Unlocked), Array.Empty())); + Encountered = new HashSet(root.GetAttributeIdentifierArray(nameof(Encountered), Array.Empty())); + Killed = new HashSet(root.GetAttributeIdentifierArray(nameof(Killed), Array.Empty())); + SyncSets(); + } + + public static void Save() + { + if (!IsInitialized) + { + throw new Exception("Creature Metrics not yet initialized!"); + } + SyncSets(); + XDocument configDoc = new XDocument(); + XElement root = new XElement("CreatureMetrics"); + configDoc.Add(root); + root.SetAttributeValue(nameof(UnlockAll), UnlockAll); + root.SetAttributeValue(nameof(Unlocked), string.Join(",", Unlocked).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Encountered), string.Join(",", Encountered).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Killed), string.Join(",", Killed).Trim().ToLowerInvariant()); + configDoc.SaveSafe(path); + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + try + { + using var writer = XmlWriter.Create(path, settings); + configDoc.WriteTo(writer); + writer.Flush(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving creature metrics failed.", e); + GameAnalyticsManager.AddErrorEventOnce("CreatureMetrics.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving creature metrics failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + + public static void RecordKill(Identifier species) + { + AddEncounter(species); + if (!Killed.Contains(species)) + { + Killed.Add(species); + } + } + + public static void AddEncounter(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Encountered.Contains(species)) { return; } + Encountered.Add(species); + RecentlyEncountered.Add(species); + UnlockInEditor(species); + } + + private static IEnumerable? vanillaCharacters; + public static void UnlockInEditor(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Unlocked.Contains(species)) { return; } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); + var contentFile = CharacterPrefab.FindBySpeciesName(species); + if (contentFile == null) { return; } + if (!vanillaCharacters.Contains(contentFile.ContentFile)) + { + // Don't try to unlock custom characters. They are always unlocked. + return; + } + Unlocked.Add(species); + } + + private static void SyncSets() + { + // Ensure that all killed are also encountered and both unlocked. + // Otherwise we could permanently hide some creatures by manually adding them to the encountered or by removing from unlocked in the xml file. + foreach (var species in Killed) + { + Encountered.Add(species); + } + foreach (var species in Encountered) + { + Unlocked.Add(species); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 5776bc9e0..2c8f0353b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -110,6 +110,7 @@ namespace Barotrauma #if CLIENT retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); + retVal.SavedCampaignSettings = element.GetChildElement("campaignsettings"); LoadSubEditorImages(element); #endif @@ -139,6 +140,9 @@ namespace Barotrauma public bool DisableInGameHints; public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; +#if CLIENT + public XElement SavedCampaignSettings; +#endif #if DEBUG public bool UseSteamMatchmaking; public bool RequireSteamAuthentication; @@ -230,7 +234,7 @@ namespace Barotrauma SoundVolume = 0.5f, UiVolume = 0.3f, VoiceChatVolume = 0.5f, - VoiceChatCutoffPrevention = 0, + VoiceChatCutoffPrevention = 200, MicrophoneVolume = 5, MuteOnFocusLost = false, DynamicRangeCompressionEnabled = true, @@ -618,6 +622,8 @@ namespace Barotrauma root.Add(inventoryKeyMappingElement); SubEditorScreen.ImageManager.Save(root); + + root.Add(CampaignSettings.CurrentSettings.Save()); #endif configDoc.SaveSafe(PlayerConfigPath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs new file mode 100644 index 000000000..55c5892e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma; + +static class ServerLanguageOptions +{ + public readonly record struct LanguageOption( + string Label, + LanguageIdentifier Identifier, + ImmutableArray MapsFrom) + { + public static LanguageOption FromXElement(XElement element) + => new LanguageOption( + Label: + element.GetAttributeString("label", ""), + Identifier: + element.GetAttributeIdentifier("identifier", LanguageIdentifier.None.Value) + .ToLanguageIdentifier(), + MapsFrom: + element.GetAttributeIdentifierArray("mapsFrom", Array.Empty()) + .Select(id => id.ToLanguageIdentifier()).ToImmutableArray()); + } + + public static readonly ImmutableArray Options; + + static ServerLanguageOptions() + { + var languageOptionElements + = XMLExtensions.TryLoadXml("Data/languageoptions.xml")?.Root?.Elements() + ?? Enumerable.Empty(); + Options = languageOptionElements + // Convert the XElements into LanguageOptions immediately since they can be worked with more directly + .Select(LanguageOption.FromXElement) + // Remove options with duplicate identifiers + .DistinctBy(p => p.Identifier) + // Remove options where the label is empty or the identifier is missing + .Where(p => !p.Label.IsNullOrWhiteSpace() && p.Identifier != LanguageIdentifier.None) + // Sort the options based on the lexicographical order of the labels + .OrderBy(p => p.Label) + .ToImmutableArray(); + } + + public static LanguageIdentifier PickLanguage(LanguageIdentifier id) + { + if (id == LanguageIdentifier.None) + { + id = GameSettings.CurrentConfig.Language; + } + + foreach (var (_, identifier, mapsFrom) in Options) + { + if (id == identifier || mapsFrom.Contains(id)) + { + return identifier; + } + } + + return TextManager.DefaultLanguage; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 706b09a0c..015a95416 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -208,16 +208,8 @@ namespace Barotrauma AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f)); Equip = element.GetAttributeBool("equip", false); - string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); - if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); - } - string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target"); - if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); - } + SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); } } @@ -1466,7 +1458,7 @@ namespace Barotrauma if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); - RegisterTreatmentResults(entity, limb, affliction, result); + RegisterTreatmentResults(user, entity as Item, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } @@ -1478,7 +1470,7 @@ namespace Barotrauma newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); - RegisterTreatmentResults(entity, limb, affliction, result); + RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } } @@ -1509,17 +1501,18 @@ namespace Barotrauma { targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } - targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality); - if (user != null && user != targetCharacter) + if (!targetCharacter.IsDead) { - if (!targetCharacter.IsDead) + float healthChange = targetCharacter.Vitality - prevVitality; + targetCharacter.AIController?.OnHealed(healer: user, healthChange); + if (user != null) { - targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality); - } - }; + targetCharacter.TryAdjustHealerSkill(user, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f); #endif + } + } } } @@ -1757,7 +1750,10 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandomUnsynced()); + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } } else { @@ -2096,14 +2092,14 @@ namespace Barotrauma if (character.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); - element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result); + element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, result.HitLimb, affliction, result); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); - element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); + element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result); } } @@ -2134,17 +2130,18 @@ namespace Barotrauma { targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } - if (element.User != null && element.User != targetCharacter) + if (!targetCharacter.IsDead) { - targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality); - if (!targetCharacter.IsDead) + float healthChange = targetCharacter.Vitality - prevVitality; + targetCharacter.AIController?.OnHealed(healer: element.User, healthChange); + if (element.User != null) { - targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality); - } - }; + targetCharacter.TryAdjustHealerSkill(element.User, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f); #endif + } + } } } } @@ -2165,18 +2162,23 @@ namespace Barotrauma private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) { - float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; - if (entity is Item sourceItem && sourceItem.HasTag("medical")) + float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem) { - multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user is not null) + if (sourceItem.HasTag("medical")) { - multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + if (user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + } + } + else if (sourceItem.HasTag(AfflictionPrefab.PoisonType) && user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - - return multiplier * AfflictionMultiplier; + return afflictionMultiplier * AfflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) @@ -2186,19 +2188,17 @@ namespace Barotrauma { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } - if (user is not null) { if (affliction.Prefab.IsBuff) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned")) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction); @@ -2206,23 +2206,25 @@ namespace Barotrauma return affliction; } - private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result) + private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result) { - if (entity is Item item && item.UseInHealthInterface && limb != null) + if (item == null) { return; } + if (!item.UseInHealthInterface) { return; } + if (limb == null) { return; } + foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) { - foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) + if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { - if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && - (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) + if (type == ActionType.OnUse || type == ActionType.OnSuccess) { - if (type == ActionType.OnUse || type == ActionType.OnSuccess) - { - limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; - } - else if (type == ActionType.OnFailure) - { - limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; - } + limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + limb.character.TryAdjustHealerSkill(user, affliction: affliction); + } + else if (type == ActionType.OnFailure) + { + limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + limb.character.TryAdjustHealerSkill(user, affliction: affliction); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 071f08337..e7071ddc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -403,7 +403,7 @@ namespace Barotrauma.Steam var ids = items.Select(it => it.Id.Value).ToHashSet(); var toUninstall = ContentPackageManager.WorkshopPackages .Where(pkg - => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + => !pkg.UgcId.TryUnwrap(out var workshopId) || !ids.Contains(workshopId.Value)) .ToArray(); if (toUninstall.Any()) @@ -495,7 +495,8 @@ namespace Barotrauma.Steam new XAttribute("corepackage", isCorePackage), new XAttribute("modversion", modVersion), new XAttribute("gameversion", gameVersion), - new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime))); + #warning TODO: stop writing Unix time after this gets on main + new XAttribute("installtime", new SerializableDateTime(updateTime).ToUnixTime())); if ((modPathDirName ?? modName).ToIdentifier() != itemTitle) { root.Add(new XAttribute("altnames", modPathDirName ?? modName)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 55e19dfad..09ebf269f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -302,6 +302,7 @@ namespace Barotrauma UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); } + // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest. if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) { UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); @@ -315,6 +316,7 @@ namespace Barotrauma } else { + // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest. if (item.Prefab.Identifier == "morbusine") { UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs index 06f36cc62..67b03d501 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -11,6 +11,7 @@ namespace Barotrauma left = l; right = r; } + // TODO: should this be && instead of ||? public override bool Loaded => left.Loaded || right.Loaded; public override void RetrieveValue() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 60a8364ca..4d7ca053c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -37,8 +37,9 @@ namespace Barotrauma public static float InterpolateRotation(float previous, float current) { + //use a somewhat high epsilon - very small differences aren't visible + if (MathUtils.NearlyEqual(previous, current, epsilon: 0.02f)) { return current; } float angleDiff = MathUtils.GetShortestAngle(previous, current); - return previous + angleDiff * (float)alpha; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index fc64098bb..88731d001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -46,7 +46,7 @@ namespace Barotrauma case int _: case double _: { - var value = (float) OriginalValue; + var value = Convert.ToSingle(OriginalValue); return level == 0 ? value : CalculateUpgrade(value, level, Multiplier); } case bool _ when bool.TryParse(Multiplier, out bool result): diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 9c31562db..610c74cc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -45,19 +45,24 @@ namespace Barotrauma public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) { - int maxLevel = Prefab.GetMaxLevelForCurrentSub(); - - if (level > maxLevel) { maxLevel = level; } - float price = BasePrice; - price += price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100f; + + int maxLevel = Prefab.MaxLevel; + + float lerpAmount = maxLevel is 0 + ? level // avoid division by 0 + : level / (float)maxLevel; + + float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount); + price += price * (priceMultiplier / 100f); + price = location?.GetAdjustedMechanicalCost((int)price) ?? price; characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characterList.Any()) { - if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive) + if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); } @@ -206,11 +211,10 @@ namespace Barotrauma _ => throw new ArgumentOutOfRangeException() }; - public bool AppliesTo(SubmarineInfo sub) + public bool AppliesTo(SubmarineClass subClass, int subTier) { if (type is MaxLevelModType.Invalid) { return false; } - int subTier = sub.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); @@ -223,9 +227,9 @@ namespace Barotrauma return subTier == tier; } - if (tierOrClass.TryGet(out SubmarineClass subClass)) + if (tierOrClass.TryGet(out SubmarineClass targetClass)) { - return sub.SubmarineClass == subClass; + return subClass == targetClass; } return false; @@ -504,15 +508,19 @@ namespace Barotrauma { int level = MaxLevel; - foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) - { - if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } - } + int tier = info.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); - level += modifier; + tier += modifier; + } + + tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier); + + foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) + { + if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); } } return level; @@ -558,7 +566,7 @@ namespace Barotrauma foreach (Item item in itemsToRemove) { - item.Remove(); + Entity.Spawner.AddItemToRemoveQueue(item); } if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs new file mode 100644 index 000000000..7d6613c33 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; +namespace Barotrauma.Utils; + +public struct CoordinateSpace2D +{ + public static readonly CoordinateSpace2D CanonicalSpace = new CoordinateSpace2D + { + Origin = Vector2.Zero, + I = Vector2.UnitX, + J = Vector2.UnitY + }; + + public Vector2 Origin; + public Vector2 I; + public Vector2 J; + + public Matrix LocalToCanonical + => new Matrix( + m11: I.X, m12: I.Y, m13: 0f, m14: 0f, + m21: J.X, m22: J.Y, m23: 0f, m24: 0f, + m31: 0f, m32: 0f, m33: 1f, m34: 0f, + m41: 0f, m42: 0f, m43: 0f, m44: 1f) + * Matrix.CreateTranslation(Origin.X, Origin.Y, 0f); + + public Matrix CanonicalToLocal => Matrix.Invert(LocalToCanonical); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index c1225eb7b..a7dedde89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -25,7 +25,7 @@ namespace Barotrauma if (!Done) { Mre.WaitOne(); } } } - private static List enqueuedTasks; + private static readonly List enqueuedTasks; static CrossThread() { enqueuedTasks = new List(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 872ba4426..76671aea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -19,6 +19,9 @@ namespace Barotrauma static class MathUtils { + public static Vector2 DiscardZ(this Vector3 vector) + => new Vector2(vector.X, vector.Y); + public static float Percentage(float portion, float total) { return portion / total * 100; @@ -152,7 +155,6 @@ namespace Barotrauma public static float CurveAngle(float from, float to, float step) { - from = WrapAngleTwoPi(from); to = WrapAngleTwoPi(to); @@ -186,13 +188,7 @@ namespace Barotrauma { return 0.0f; } - - while (angle < 0) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.TwoPi) - angle -= MathHelper.TwoPi; - - return angle; + return PositiveModulo(angle, MathHelper.TwoPi); } /// @@ -204,13 +200,9 @@ namespace Barotrauma { return 0.0f; } - // Ensure that -pi <= angle < pi for both "from" and "to" - while (angle < -MathHelper.Pi) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.Pi) - angle -= MathHelper.TwoPi; - - return angle; + float min = -MathHelper.Pi; + float diffFromMin = angle - min; + return diffFromMin - (MathF.Floor(diffFromMin / MathHelper.TwoPi) * MathHelper.TwoPi) + min; } public static float GetShortestAngle(float from, float to) @@ -339,13 +331,13 @@ namespace Barotrauma if (axisAligned1.Y < axisAligned2.Y) { - if (y < axisAligned1.Y) return false; - if (y > axisAligned2.Y) return false; + if (y < axisAligned1.Y) { return false; } + if (y > axisAligned2.Y) { return false; } } else { - if (y > axisAligned1.Y) return false; - if (y < axisAligned2.Y) return false; + if (y > axisAligned1.Y) { return false; } + if (y < axisAligned2.Y) { return false; } } intersection = new Vector2(axisAligned1.X, y); @@ -361,13 +353,13 @@ namespace Barotrauma if (axisAligned1.X < axisAligned2.X) { - if (x < axisAligned1.X) return false; - if (x > axisAligned2.X) return false; + if (x < axisAligned1.X) { return false; } + if (x > axisAligned2.X) { return false; } } else { - if (x > axisAligned1.X) return false; - if (x < axisAligned2.X) return false; + if (x > axisAligned1.X) { return false; } + if (x < axisAligned2.X) { return false; } } intersection = new Vector2(x, axisAligned1.Y); @@ -898,23 +890,30 @@ namespace Barotrauma // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f) { - float diff = Math.Abs(a - b); if (a == b) { - // shortcut, handles infinities + //shortcut, handles infinities return true; } - else if (a == 0 || b == 0 || diff < float.Epsilon) + + if (a == 0 || b == 0) { - // a or b is zero or both are extremely close to it - // relative error is less meaningful here - return diff < epsilon; + //if a or b is zero, relative error is less meaningful + return Math.Abs(a - b) < epsilon; } - else + + float absA = Math.Abs(a); + float absB = Math.Abs(b); + float absAB = absA + absB; + if (absAB < epsilon) { - // use relative error - return diff / (Math.Abs(a) + Math.Abs(b)) < epsilon; + // a and b extremely close to zero, relative error is less meaningful + return true; } + + float diff = Math.Abs(a - b); + // use relative error + return diff / absAB < epsilon; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index 9fdc1e786..9bc971557 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -1,9 +1,7 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Barotrauma.IO; +using System; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -12,52 +10,6 @@ namespace Barotrauma { public class Md5Hash { - public static class Cache - { - private const string cachePath = "Data/hashcache.txt"; - - private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries - = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); - - public static void Load() - { - if (!File.Exists(cachePath)) { return; } - var lines = File.ReadAllLines(cachePath); - if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) - { - for (int i = 1; i < lines.Length; i++) - { - string[] split = lines[i].Split('|'); - string path = split[0].CleanUpPathCrossPlatform(); - Md5Hash hash = Md5Hash.StringAsHash(split[1]); - DateTime? dateTime = null; - if (long.TryParse(split[2], out long dateTimeUlong)) - { - dateTime = DateTime.FromBinary(dateTimeUlong); - } - - if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) - { - Entries.Add((path, hash, dateTime.Value)); - } - } - } - } - - public static void Add(string path, Md5Hash hash, DateTime dateTime) - { - path = path.CleanUpPathCrossPlatform(); - Remove(path); - Entries.Add((path, hash, dateTime)); - } - - public static void Remove(string path) - { - path = path.CleanUpPathCrossPlatform(); - Entries.RemoveAll(e => e.Path == path); - } - } - public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); private static string RemoveWhitespace(string s) @@ -212,8 +164,13 @@ namespace Barotrauma return false; } + public override int GetHashCode() + { + return ShortRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + public static bool operator ==(Md5Hash? a, Md5Hash? b) - => (a is null == b is null) && (a?.Equals(b) ?? true); + => Equals(a, b); public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs deleted file mode 100644 index db6e813b9..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - public sealed class None : Option - { - private None() { } - - public static Option Create() => new None(); - - public override Option Fallback(Option fallback) => fallback; - public override T Fallback(T fallback) => fallback; - - public override bool ValueEquals(T value) => false; - - public override string ToString() - => $"None<{typeof(T).Name}>"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 9aff08c3f..112281e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,60 +1,83 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { - /// - /// Implementation of Option type. - /// - /// - /// Credit Jlobblet - /// - public abstract class Option + public readonly struct Option where T : notnull { - public static Option Some(T value) => Some.Create(value); - public static Option None() => None.Create(); - public bool IsNone() => this is None; - public bool IsSome() => this is Some; + private readonly bool hasValue; + private readonly T? value; - public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); - - public bool TryUnwrap(out T1 outValue) where T1 : T + private Option(bool hasValue, T? value) { - switch (this) - { - case Some { Value: T1 value }: - outValue = value; - return true; - default: - outValue = default!; - return false; - } + this.hasValue = hasValue; + this.value = value; } - public Option Select(Func selector) => - this switch + public bool IsSome() => hasValue; + public bool IsNone() => !IsSome(); + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T + { + bool hasValueOfGivenType = false; + outValue = default; + + if (hasValue && value is T1 t1) { - Some { Value: var value } => Option.Some(selector.Invoke(value)), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException() + hasValueOfGivenType = true; + outValue = t1; + } + + return hasValueOfGivenType; + } + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue) + => TryUnwrap(out outValue); + + public Option Select(Func selector) where TType : notnull + => TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None; + + public Option Bind(Func> binder) where TType : notnull + => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + + public T Fallback(T fallback) + => TryUnwrap(out var v) ? v : fallback; + + public Option Fallback(Option fallback) + => IsSome() ? this : fallback; + + public static Option Some(T value) + => typeof(T) switch + { + var t when t == typeof(bool) + => throw new Exception("Option type rejects booleans"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>) + => throw new Exception("Option type rejects nested Option"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>) + => throw new Exception("Option type rejects Nullable"), + _ + => new Option(hasValue: true, value: value ?? throw new Exception("Option type rejects null")) }; - public abstract Option Fallback(Option fallback); - public abstract T Fallback(T fallback); - - public abstract bool ValueEquals(T value); - public override bool Equals(object? obj) => obj switch { - Some { Value: var value } => this is Some { Value: { } selfValue } && selfValue.Equals(value), - None _ => IsNone(), - T value => this is Some { Value: { } selfValue } && selfValue.Equals(value), - _ => false + Option otherOption when otherOption.IsNone() + => IsNone(), + Option otherOption when otherOption.TryUnwrap(out var otherValue) + => ValueEquals(otherValue), + T otherValue + => ValueEquals(otherValue), + _ + => false }; + public bool ValueEquals(T otherValue) + => TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue); + public override int GetHashCode() - => this is Some { Value: { } value } ? value.GetHashCode() : 0; + => TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0; public static bool operator ==(Option a, Option b) => a.Equals(b); @@ -62,22 +85,28 @@ namespace Barotrauma public static bool operator !=(Option a, Option b) => !(a == b); - public abstract override string ToString(); - - public static implicit operator Option(Option.UnspecifiedNone _) + public static Option None() + => default; + + public static implicit operator Option(in Option.UnspecifiedNone _) => None(); + + public override string ToString() + => TryUnwrap(out var selfValue) + ? $"Some<{typeof(T).Name}>({selfValue})" + : $"None<{typeof(T).Name}>"; } public static class Option { - public sealed class UnspecifiedNone + public static Option Some(T value) where T : notnull + => Option.Some(value); + + public static UnspecifiedNone None + => default; + + public readonly ref struct UnspecifiedNone { - private UnspecifiedNone() { } - internal static readonly UnspecifiedNone Instance = new(); } - - public static UnspecifiedNone None => UnspecifiedNone.Instance; - - public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs deleted file mode 100644 index 5fd1dc3b0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Barotrauma -{ - public sealed class Some : Option - { - public readonly T Value; - - private Some(T value) - { - if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } - Value = value; - } - - public static Option Create(T value) => new Some(value); - - public override Option Fallback(Option fallback) => this; - public override T Fallback(T fallback) => Value; - - public override bool ValueEquals(T value) => Value.Equals(value); - - public override string ToString() - => $"Some<{typeof(T).Name}>({Value})"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 3c6b11b64..9429dcf93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -23,7 +23,7 @@ namespace Barotrauma return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); } - public static Option ParseDerived(TInput input) where TInput : notnull + public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull { static Option none() => Option.None(); @@ -54,10 +54,10 @@ namespace Barotrauma f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) - as Option ?? none(); + as Option? ?? none(); } - return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); + return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()); } public static string NameWithGenerics(this Type t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 55120383d..f4adb8a06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using System.Text.RegularExpressions; using Barotrauma.IO; using Microsoft.Xna.Framework; +using System.Collections.Immutable; namespace Barotrauma { @@ -292,10 +293,11 @@ namespace Barotrauma } if (doc?.Root == null) { - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: Option.None, + SubmarineName: "", + EnabledContentPackageNames: ImmutableArray.Empty)); } else { @@ -326,13 +328,11 @@ namespace Barotrauma enabledContentPackageNames.Add(packageName.Replace(@"\|", "|")); } - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file, - SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), - SaveTime = doc.Root.GetAttributeInt("savetime", 0), - EnabledContentPackageNames = enabledContentPackageNames.ToArray(), - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: doc.Root.GetAttributeDateTime("savetime"), + SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), + EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray())); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs index 63f652f57..556ca0fd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Networking; */ [NetworkSerialize] -public readonly record struct Segment(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct; +public readonly record struct Segment(T Identifier, int Pointer) : INetSerializableStruct where T : struct; readonly ref struct SegmentTableWriter where T : struct { @@ -94,7 +94,7 @@ readonly ref struct SegmentTableWriter where T : struct public static SegmentTableWriter StartWriting(IWriteMessage msg) { var retVal = new SegmentTableWriter(msg, msg.BitPosition); - msg.WriteUInt16(0); //reserve space for the table pointer + msg.WriteInt32(0); //reserve space for the table pointer return retVal; } @@ -104,28 +104,22 @@ readonly ref struct SegmentTableWriter where T : struct { throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>"); } - - if (message.BitPosition - PointerLocation > UInt16.MaxValue) - { - throw new OverflowException( - $"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)"); - } } - + public void StartNewSegment(T value) { ThrowOnInvalidState(); - segments.Add(new Segment(value, (UInt16)(message.BitPosition-PointerLocation))); + segments.Add(new Segment(value, message.BitPosition - PointerLocation)); } public void Dispose() { ThrowOnInvalidState(); int tablePosition = message.BitPosition; - + //rewrite the table pointer now that we know where the table ends message.BitPosition = PointerLocation; - message.WriteUInt16((UInt16)(tablePosition-PointerLocation)); + message.WriteInt32(tablePosition - PointerLocation); //write the table message.BitPosition = tablePosition; @@ -274,7 +268,7 @@ readonly ref struct SegmentTableReader where T : struct ExceptionHandler? exceptionHandler = null) { int pointerLocation = msg.BitPosition; - int tablePointer = msg.ReadUInt16(); + int tablePointer = msg.ReadInt32(); int tableLocation = pointerLocation + tablePointer; int returnPosition = msg.BitPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs new file mode 100644 index 000000000..49c50dc1f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -0,0 +1,265 @@ +#nullable enable +using System; +using System.Globalization; +using System.Linq; + +namespace Barotrauma +{ + public readonly struct SerializableTimeZone + { + /// + /// Diff from UTC + /// + public readonly TimeSpan Value; + + private readonly int hours; + private readonly int minutes; + private readonly char sign; + + public SerializableTimeZone(TimeSpan value) + { + Value = new TimeSpan( + hours: value.Hours, + minutes: value.Minutes, + seconds: 0); + + hours = Math.Abs(value.Hours); + minutes = Math.Abs(value.Minutes); + sign = Value.Ticks < 0 ? '-' : '+'; + } + + public override string ToString() + => (hours, minutes) switch + { + (0, 0) => "UTC", + (_, 0) => $"UTC{sign}{hours}", + (_, < 10) => $"UTC{sign}{hours}:0{minutes}", + _ => $"UTC{sign}{hours}:{minutes}" + }; + + public override int GetHashCode() + => HashCode.Combine(Value.Ticks < 0, hours, minutes); + + public static SerializableTimeZone FromDateTime(DateTime dateTime) + { + if (dateTime.Kind == DateTimeKind.Unspecified) + { + throw new InvalidOperationException( + $"Cannot determine timezone for {nameof(DateTime)} " + + $"of unspecified kind"); + } + var utcDateTime = dateTime.ToUniversalTime(); + return new SerializableTimeZone(dateTime - utcDateTime); + } + + public static SerializableTimeZone LocalTimeZone + => FromDateTime(DateTime.Now); + + public static Option Parse(string str) + { + if (!str.StartsWith("UTC", StringComparison.OrdinalIgnoreCase)) + { + return Option.None(); + } + string timeZoneStr = str[3..]; + bool negative = timeZoneStr.StartsWith("-"); + bool valid = negative || timeZoneStr.StartsWith("+"); + + if (!valid) { return Option.None(); } + + timeZoneStr = str[4..]; + + TimeSpan makeTimeSpan(int hours, int minutes) + => new TimeSpan( + ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute) + * (negative ? -1L : 1L)); + + if (timeZoneStr.IndexOf(':') is var hrMinSeparator && hrMinSeparator > 0) + { + if (int.TryParse(timeZoneStr[..hrMinSeparator], out int timeZoneHours) + && int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out int timeZoneMinutes)) + { + return Option.Some( + new SerializableTimeZone(makeTimeSpan(timeZoneHours, timeZoneMinutes))); + } + } + else if (int.TryParse(timeZoneStr, out int timeZoneHours)) + { + return Option.Some( + new SerializableTimeZone(makeTimeSpan(timeZoneHours, 0))); + } + return Option.None(); + } + } + + /// + /// DateTime wrapper that tries to offer a reliable + /// string representation that's also human-friendly + /// + public readonly struct SerializableDateTime : IComparable + { + public bool Equals(SerializableDateTime other) + => ToUtc().value.Equals(other.ToUtc().value); + + public override bool Equals(object? obj) + => obj is SerializableDateTime other && Equals(other); + + private static DateTime UnixEpoch(DateTimeKind kind) + => new DateTime(1970, 1, 1, 0, 0, 0, kind); + + private readonly DateTime value; + public readonly SerializableTimeZone TimeZone; + + public SerializableDateTime(DateTime value) : this(value, default) + { + if (value.Kind == DateTimeKind.Unspecified) + { + throw new Exception($"Timezone required when constructing {nameof(SerializableDateTime)} " + + $"from {nameof(DateTime)} of unspecified kind"); + } + TimeZone = SerializableTimeZone.FromDateTime(value); + } + + public SerializableDateTime(DateTime value, SerializableTimeZone timeZone) + { + this.value = new DateTime( + value.Year, value.Month, value.Day, + value.Hour, value.Minute, value.Second, + DateTimeKind.Unspecified); + TimeZone = timeZone; + } + + public static SerializableDateTime LocalNow + => new SerializableDateTime(DateTime.Now); + + public static SerializableDateTime UtcNow + => new SerializableDateTime(DateTime.UtcNow); + + public SerializableDateTime ToUtc() + => new SerializableDateTime( + DateTime.SpecifyKind(value - TimeZone.Value, DateTimeKind.Utc)); + + public SerializableDateTime ToLocal() + => new SerializableDateTime( + new DateTime(ticks: value.Ticks) - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + SerializableTimeZone.LocalTimeZone); + + public long Ticks => value.Ticks; + + public DateTime ToUtcValue() => ToUtc().value; + public DateTime ToLocalValue() => ToLocal().value; + + public static SerializableDateTime FromLocalUnixTime(long unixTime) + => new SerializableDateTime(UnixEpoch(DateTimeKind.Local) + TimeSpan.FromSeconds(unixTime)); + + public static SerializableDateTime FromUtcUnixTime(long unixTime) + => new SerializableDateTime(UnixEpoch(DateTimeKind.Utc) + TimeSpan.FromSeconds(unixTime)); + + public long ToUnixTime() + => (value - UnixEpoch(value.Kind)).Ticks / TimeSpan.TicksPerSecond; + + private static string MakeString(params (long Value, string Suffix)[] parts) + => string.Join(' ', + parts.Select(p => $"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}")); + + public override string ToString() + => MakeString( + // Let's go out of our way to tag + // the year, month and day so nobody + // gets confused about the meaning of + // each number + (value.Year, "Y"), + (value.Month, "M"), + (value.Day, "D"), + + (value.Hour, "HR"), + (value.Minute, "MIN"), + (value.Second, "SEC")) + + $" {TimeZone}"; + + public string ToLocalUserString() + => ToLocalValue().ToString(CultureInfo.InvariantCulture); + + public override int GetHashCode() + => HashCode.Combine( + value.Year, value.Month, value.Day, + value.Hour, value.Minute, value.Second, + TimeZone.GetHashCode()); + + public static Option Parse(string str) + { + if (long.TryParse(str, out long unixTime) + && unixTime > 0 + && unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds) + { + return Option.Some(FromUtcUnixTime(unixTime)); + } + + string[] split = str.Split(' '); + + int year = 0; int month = 0; int day = 0; + int hour = 0; int minute = 0; int second = 0; + SerializableTimeZone timeZone = default; + foreach (var part in split) + { + if (SerializableTimeZone.Parse(part).TryUnwrap(out var parsedTimeZone)) + { + timeZone = parsedTimeZone; + continue; + } + + Identifier suffix = string.Join("", part.Where(char.IsLetter)).ToIdentifier(); + if (!part.EndsWith(suffix.Value)) { continue; } + if (!int.TryParse( + part[..^suffix.Value.Length], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int value)) + { + continue; + } + + if (suffix == "Y") { year = value; } + else if (suffix == "M") { month = value; } + else if (suffix == "D") { day = value; } + else if (suffix == "HR") { hour = value; } + else if (suffix == "MIN") { minute = value; } + else if (suffix == "SEC") { second = value; } + } + + if (year > 0 && month > 0 && day > 0) + { + return Option.Some( + new SerializableDateTime( + new DateTime(year, month, day, hour, minute, second), + timeZone)); + } + + return Option.None(); + } + + public int CompareTo(SerializableDateTime other) + => ToUtc().value.CompareTo(other.ToUtc().value); + + public static bool operator <(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) < 0; + + public static bool operator >(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) > 0; + + public static bool operator ==(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) == 0; + + public static bool operator !=(in SerializableDateTime a, in SerializableDateTime b) + => !(a == b); + + public static SerializableDateTime operator +(in SerializableDateTime dt, in TimeSpan ts) + => new SerializableDateTime(dt.value + ts, dt.TimeZone); + + public static SerializableDateTime operator -(in SerializableDateTime dt, in TimeSpan ts) + => new SerializableDateTime(dt.value - ts, dt.TimeZone); + + public static TimeSpan operator -(in SerializableDateTime a, in SerializableDateTime b) + => a.ToUtc().value - b.ToUtc().value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index a7d065ed0..08254fa0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -29,49 +29,6 @@ namespace Barotrauma static partial class ToolBox { - internal static class Epoch - { - private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - /// - /// Returns the current Unix Epoch (Coordinated Universal Time) - /// - public static int NowUTC - { - get - { - return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds); - } - } - - /// - /// Returns the current Unix Epoch (user's current time) - /// - public static int NowLocal - { - get - { - return (int)(DateTime.Now.Subtract(epoch).TotalSeconds); - } - } - - /// - /// Convert an epoch to a datetime - /// - public static DateTime ToDateTime(decimal unixTime) - { - return epoch.AddSeconds((long)unixTime); - } - - /// - /// Convert a DateTime to a unix time - /// - public static uint FromDateTime(DateTime dt) - { - return (uint)(dt.Subtract(epoch).TotalSeconds); - } - } - public static bool IsProperFilenameCase(string filename) { //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds. diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 2193e4329..15470c0cb 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,182 +1,325 @@ --------------------------------------------------------------------------------------------------------- -v100.13.0.0 +v1.0.1.0 --------------------------------------------------------------------------------------------------------- -- Fixes included in versions up to v0.21.0.0. -- Added translations for the endgame and faction content. -- Fixed reputation resetting between rounds. -- Added a round light component variant. -- Fixed z-fighting in HuskDistrict_Module_02. -- Fixed missing gaps in the Winterhalter wreck in the end level. -- Fixed end levels still sometimes including normal level geometry in saves started in the stable version. +- Fixed Mailman talent giving you the 150 mk bonus every time you open the campaign map or mission menu and there's a cargo mission visible. +- Fixed missions available from the destination location to some other location being listed as "outpost missions" in the campaign map's mission selection. +- Fixed outposts that faction missions take place in being allowed to turn into abandoned outposts when next to hunting grounds, making the missions impossible to complete. +- Fixed hidden items (e.g. Separatist deco that's disabled in Coalition outposts) sometimes getting chosen as targets for scripted events, resulting in non-interactable, glowing "ghost items". +- Fixed mysterious floating status monitor in AdminModule_02_Colony. +- Miscellaneous optimizations. +- Fixed Separatist jailbreak mission causing a crash. +- Fixed ranged weapons (most noticeably, scrap cannon) emitting particles in the wrong direction. +- Fixed acid burns not having a cause of death text. +- Fixed "skedaddle" not giving a 10% movement boost like the description says. +- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. +- Fixed red glow around the light switch's green button. +- Fixed banana being held weirldy. +- Fixed inability to hold a captain's pipe or cigar in your left hand. +- Fixed ready checks not working. --------------------------------------------------------------------------------------------------------- -v100.12.0.0 +v1.0.0.0 --------------------------------------------------------------------------------------------------------- -- Fixes included in versions up to v0.20.15.0. -- Fixed faction-specific bots not getting the correct ID card tags if they spawn for the first time in an outpost (= if you save and quit and reload in an outpost). -- The "x reputation needed to unlock some biome" text is hidden in the tab menu if some other faction has already unlocked the path. -- Fixed NPC following the player indefinitely in the "way to ascension" event in which you need to deliver husk eggs. -- Fixed "Campaigning" talent not giving a discount on upgrades. -- Fixed items appearing stolen if you enter another outpost that happens to have the same name as the one you stole the items from. -- The generic outpost NPC greeting lines ("nice to see a friendly face", etc) aren't used when you have negative reputation. -- Fixed ladders being drawn in front of the docking hatch in DockingModule_01.. -- Fixed Subra assassination events working even if you've started the main husk cult chain (or even after you've hired Subra) -- Fixed submarine name being displayed as "Unknown" in the ending montage. -- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. -- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. -- Adjusted the layout of the research beacon station to prevent the entrance from ending up in an impossible-to-reach spot, made beacon station docking ports visible on sonar. -- Fixed missing gaps in the end level's wrecks. -- Fixed ancient weapons not damaging doors anymore. -- Reputation rewards are displayed in the round summary. -- Fixed missions that are configured to give location reputation (= reputation for the faction controlling the location you got the mission from) sometimes not giving rep. Happened because the game assumed the correct location is the one at the start of the connection the mission takes place in, which may not be the case (the location may have been unlocked further on the map, or it may be at the end of the connection). -- Fixed mini nuke explosion sound. -- Fixed "could not determine reputation change" console warnings when checking reputation in the tab menu or round summary during the 1st round in MP. -- Fixed campaign map sometimes displaying incorrect reputation values in multiplayer. - ---------------------------------------------------------------------------------------------------------- -v100.11.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixes included in v0.20.12.0. - ---------------------------------------------------------------------------------------------------------- -v100.10.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixes included in v0.20.11.0. - ---------------------------------------------------------------------------------------------------------- -v100.9.0.1 ---------------------------------------------------------------------------------------------------------- - -- Fixes included in v0.20.10.1. - ---------------------------------------------------------------------------------------------------------- -v100.9.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixes included in v0.20.10.0. -- Fixed genetic materials still being available for sale without the "Blackmarket Genes" talent. - ---------------------------------------------------------------------------------------------------------- -v100.8.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixed vanilla package failing to load due to a duplicate music clip in sounds.xml, leading to a crash on startup. - ---------------------------------------------------------------------------------------------------------- -v100.7.0.0 ---------------------------------------------------------------------------------------------------------- - -No changes aside from the fixes in v0.20.9.0. - ---------------------------------------------------------------------------------------------------------- -v100.6.0.0 ---------------------------------------------------------------------------------------------------------- - -- Some new music and sounds for the final levels. -- Some new loading screen portraits and improvements to the existing ones. -- Visual improvements to the final levels. -- Visual improvements to the location info overlays on the campaign map. -- The "exit" sonar marker doesn't appear in the end levels until you've pressed the switch that opens the exit, and once you do, the switch marker disappears. -- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. -- Made some talent items purchaseable with high faction rep. -- Adjustments to outpost events' reputation rewards. -- Adjustments to the reputation thresholds at which some husk cult events can trigger. Previously you needed at least 15 rep to start the main event chain, which was very hard to reach because there's so few ways to gain cultist rep outside the event chain. -- Gave Captain Hognose some experience points. -- Ritual lanterns heal all afflictions of the type "damage", not just organ damage and burns. -- Fixed cultist robes hiding iron helmet. -- Fixed FB3000 being set to autoinject, making it use up the fuel rod when below 50% health. - ---------------------------------------------------------------------------------------------------------- -v100.5.0.0 ---------------------------------------------------------------------------------------------------------- - -- Optimized the projectiles in the final level (getting hit by one should no longer turn the game into a slideshow). -- Fixed misaligned ladded in EngineeringModule_01_Colony. -- Some new music tracks and loading screen portraits. - ---------------------------------------------------------------------------------------------------------- -v100.4.0.0 ---------------------------------------------------------------------------------------------------------- - -- Updated loading screen and main menu graphics. -- Hid some of the endgame items from the sub editor. -- Fixed research beacon station sometimes spawning in normal levels. -- Fixed research beacon station's generator not generating any power. -- Fixed end boss sometimes spawning a bit off from the intended spawn position. - ---------------------------------------------------------------------------------------------------------- -v100.3.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixed crashing when you enter the shipyard interface. - ---------------------------------------------------------------------------------------------------------- -v100.2.0.0 ---------------------------------------------------------------------------------------------------------- - -- Added a new ending for the campaign. -- New loading screen / location portraits. - -Fixes: -- Fixed cultist robe's hood rendering over diving suit helmets. -- Fixed colonies sometimes failing to generate properly (falling back to a small pre-made outpost). -- Fixed some of the hireable characters spawning without a headset. -- Made honker blast require and consume fuel to prevent it from being infinitely spammable. -- Fixed NPCs not attacking you in the Subra rescue mission. -- Fixed "no mission selected" popup being shown if there's no missions available for the next round, but an outpost mission available in the destination. -- Fixed mining mission sonar labels displaying as [resourcename]. - ---------------------------------------------------------------------------------------------------------- -v100.1.0.0 ---------------------------------------------------------------------------------------------------------- - -- Fixed all colonies having clown deco in them. -- Made Funbringer 3000 craftable. -- Fixed Clownhaven mission sometimes unlocking in non-colony outposts. -- Fixed texts duplicating after each round in the Book of Chalices and Honkmotherian Scriptures. -- Fixed bots fleeing from the huskified storage container. -- Fixed console error about "missionevent_escort1" when entering the 2nd outpost. - ---------------------------------------------------------------------------------------------------------- -v100.0.0.0 ---------------------------------------------------------------------------------------------------------- - -Test version of the faction overhaul: +Faction overhaul: - Outposts are controlled by the Europa Coalition or Jovian Separatists, and some of them include a module belonging to the Church of Husk or Children of the Honkmother. -- Lots of new outpost events, and a longer "event chain" for the secondary factions. -- Lots of new facion-specific missions: some variants of existing missions, some new. - Got rid of location-specific reputation. Now all the events/missions give faction reputation instead (excluding missions that aren't related to or given by a faction, e.g. abandoned outpost missions). +- Lots of new outpost events, and a longer "event chain" for the secondary factions. +- Lots of new faction-specific missions: some variants of existing missions, some new. - Faction-specific hires: "generic" high-level characters with more experience points and better gear than normal hireable NPCs. Available for hiring when Coalition or Separatist reputation is high enough. - Special, named characters who can be hired via scripted events after reaching a high enough reputation. - Faction-specific vendors (separatists, husks, clowns) who sell special items (many of which are completely new) if your reputation is high enough. - If your Coalition/Separatist reputation is low enough, you may get attacked by their vessel during missions. -- There's now always two paths from biome to another, one controlled by the Coalition and one by Separatists. +- There's now always two paths from biome to another, one controlled by the Coalition and one by the Separatists. - Improvements to the campaign map. +- Added a 3rd talent tree, "Politician", for the Captain. Focused around faction relations and reputation. + +Endgame: +- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels or radiation. +- New types of enemies/bosses. +- Some new events to foreshadow the ending during the course of the campaign. + +Misc changes: +- New loading screen / location portraits. +- Two new music tracks. +- Items' skill requirements are shown in their tooltips (the same way as damage resistances). +- Tweaks to poisons. +- Adjusted Europan Handshake to work better with the overhauled morbusine poisoning. +- Acid Grenades and 40mm Acid Grenades are now properly affected by talents +- Acid Grenades and 40mm Acid Grenades deal more damage and slow enemies down, making them more viable against fast monsters. +- Made regular 40mm grenades penetrate armor more efficiently. +- Made Diving Suits resist Acid Burns a bit more. +- Europa Brew's Acid Vulnerability is now double as effective (200% damage taken instead of 100%). +- Adjustment to throwable items (shorter throw distance and reduced speed in water). +- Made flares float in place to make them more useful. +- Made high-quality stun guns more effective (stunning the target faster). +- The health scanner always shows poisons and paralysis on monsters to make it easier to determine whether the poisoning is progressing or wearing off. +- A pass on sound ranges: the ranges should now be more consistent and sensible. +- Made moloch shell fragment and riot shield as medium items instead of small to fix them going inside e.g. toolbelts. +- Made husk eggs consumable. +- Made it more difficult to repeatedly enter an abandoned outpost and re-loot the bandits: now the bandits immediately attack you if you re-enter the outpost. +- Monsters you haven't encountered yet are now hidden by default in the character editor. Can be enabled using the command "showmonsters" and re-hidden using "hidemonsters". The value is saved in creaturemetrics.xml. Doesn't affect custom creatures. +- Fixed character crush depths behaving inconsistently (varying between levels, e.g. sometimes crushing the character at the depth of 2000 meters, sometimes 3000). +- Improvements to submarine crush depth effects: previously the breaches were easy to deal with because pressure did small amounts of damage to all walls, now it instead does heavier damage to some walls (and the amount of damage and walls to damage increases with depth). +- Added a round light component variant. +- Increased the hard-coded max mission count back from 3 to 10. It'd be preferable to not change the value above 3 in the vanilla game, but since campaign settings are not moddable, we shouldn't be too strict about it (because it can be useful for a mod that this value can be adjusted). +- Miscellaneous optimizations. +- New slot indicator icons (= the icons that show what can go inside some item, like tanks/ammo). +- Made outpost hull repair service cheaper. +- Doors can now be damaged by melee weapons and ranged (handheld) weapons. (They were already destructible by submarine mounted weapons and explosives) +- Adjusted and rebalanced item damage for most items, to take into account doors being destructible. +- Reduced time needed for a crowbar to open doors, 7.5s for regular doors, 6s for wrecked doors (down from 10 s). +- Boosted Plasma Cutter damage against doors and items (walls not touched). +- Made galena more common to make lead easier to get. +- Recreated all and added new slot indicator icons to provide information about the containable item restrictions. +- Added Auto Operate option for all turrets. Can be enabled in the submarine editor. Not currently used on vanilla submarines. Auto operated turrets don't require a person to operate them, but they still require power and ammunition (-> someone needs to reload them). + +Multiplayer: +- Added a language filter to the server browser. +- Fixed reports given by dragging and dropping them on the status monitor always targeting the room the character is inside. +- Improvements to medical clinic syncing (should fix some of the afflictions a character has sometimes not being visible on the list). +- Fixed crashing if you close a server when mod downloads are disabled. +- Improved projectile syncing: spread now behaves the same client-side as it does server-side (as opposed to being completely random). +- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. +- Fixed server randomizing the game mode at the end of the round when playing a campaign with the game mode selection set to Random. + +AI: +- Fixed bots considering certain multi-hull rooms flooded when they are not. +- Fixed bots deciding prematurely that they can't fix an item when it's deteriorating (e.g. when it's submerged). +- Fixed bots removing battery cells from exosuits when ordered to charge batteries. +- Fixed bots sometimes getting stuck in automatic doors and/or double doors, because they didn't wait for the door to open entirely before pressing the button again. +- Improved bot extinguish fires behavior. Fixes bots sometimes not being able to extinguish larger fires, because they stopped too far and didn't keep advancing towards the target. +- Fixed bots claiming that they can't return back to the sub and then following the order anyway. +- Improved the find safety calculations so that the bots give more preference to the distance of the room. +- Fixed some remaining issues and edge cases in the logic over when the bot needs diving gear and when it can be taken off. +- Fixed captains (and some NPCs) idling in the airlock if they equip a diving suit. +- Bot can now target items (like projectiles) with turrets and have different targeting priorities on different monsters. +- Fixed bots being allowed to reach items that are too far to be interacted with. + +Fixes: +- Fixes and improvements to translations (Japanese and Chinese in particular). +- Fixed light components with a range of 0 and a hidden sprite being invisible against dark backgrounds. +- Various fixes to Typhon 1: most notably, adjusting the hulls to prevent some rooms from being impossible to drain fully. +- Fixed "kill" command not killing characters under the influence of "Miracle Worker". +- Fixed some lights becoming invisible when their range is set to 0 and they're against a dark background. +- Fixed lights turning on without power when they receive a toggle or set_color input. +- Fixed changing the amount to fabricate starting fabrication in MP if you've previously started fabricating something. +- Fixed campaign settings resetting in the campaign setup menu every time you relaunch the game (meaning you'd always need to e.g. remember to toggle the tutorial off if you want to play without it). +- Fixed inverted mouse buttons not working properly since the last update: the left mouse button was considered the primary mouse button regardless of your OS settings. +- Fixed status monitor not properly displaying condition on tinkered items. +- Fixed machines at above 100% condition with tinkering smoking. +- Fixed inventory overlapping with the chatbox on low aspect ratios (small width, large height). +- Fixed some layering issues in abandoned outposts. +- Fixed water-sensitive items sometimes spawning as loot in wrecks. +- Fixed radio static still playing even if you don't have a headset. +- Fixed rifle grenade sounds not working. +- Fixed crashing on startup if the MD5 hash cache file is empty. +- Fixed research stations and loaders not being visible on the status monitor's electrical view. +- Fixed artifact missions sometimes choosing the same artifact as a target if you happen to have multiple missions active at a time, which would lead to console errors when the round ends. +- Fixed exosuit playing the warning beep if there's empty or almost empty tanks in any of it's slots. +- Fixed oxygen generators deteriorating in some of the outpost modules. +- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. +- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. +- Fixed characters trying to walk in flooded spaces that are too low to stand in (like some of the tight passages in alien ruins). +- Genetic material backwards compatibility to fix old unidentified genetic materials disappearing from saves prior to v0.21.6.0. +- Fixed genetic materials being too rare in outposts now. +- Fixed hunting grounds affecting outposts 2 steps away, not just ones in adjacent locations. +- Fixed "residual waste" talent duplicating genetic materials. +- Fixed monsters sometimes spawning inside destructible ice chunks in caves. +- Fixed respawn shuttle sometimes spawning inside floating ice chunks. +- Fixed equipping a ranged weapon setting its reload timer to 1, making it possible to reduce some weapons' loading times by unequipping and equipping them. +- Fixed partially consumed items not staying on top of the stack they're in. +- Fixed submarine tier and class affecting the prices of the submarine upgrades: e.g. a tier 2 upgrade would cost more on a submarine where tier 2 is the maximum than on a submarine with a higher maximum. +- Fixed reputation loss when you steal items from bandits in a beacon station. +- Fixed equipped flares igniting when you click on the inventory. +- Fixed "Quickdraw" talent not affecting Alien Pistols. +- Fixed toolbelts and other items worn on the torso getting hidden when wearing a safety harness. +- Fixed advanced syringe gun and slipsuit fabrication recipes. +- Fixed floating pumps and ladder layering issues in Herja. +- Limited the number of makeshift shelves per sub to 3 (similar to portable pumps). Otherwise you can use them to expand the sub's cargo capacity indefinitely. +- Fixed dementonite and hardened crowbars spawning in respawn containers (= respawn shuttle cabinets). +- Fixed alien blood no longer causing psychosis, + made it slightly less effective to make fabricating blood packs more worthwhile +- Fixed fire extinguisher spray getting blocked by characters. +- Venture: Fixed the battery room not flooding properly (again), fixed the two hulls in the airlock not being linked, adjusted the waypoints a bit. +- Selkie: Disconnect the outer nodes from the ladder/door nodes, because the docking ports can't be opened manually. +- Fixed Thalamus AI not running properly when there's no player characters or submarines around (e.g. when all the players are in the freecam mode). +- Fixed the ammo indicator not showing correctly on advanced syringe gun. +- Fixed bots sometimes getting confused by outside waypoints while being inside an outpost. +- Fixed item relocation logic running also on NPCs that are not in the player team, which could cause diving suits dropped by NPCs to get spawned in the player sub. + +Modding: +- Fixed crashing if a StatusEffect is configured to SpawnItemRandomly but doesn't configure anything to spawn. +- Improved the error handling of item/character variants. Previously if the parent prefab wasn't found, there was no error message, but the variant was still created, causing crashes in various situations. +- Added AITurretPriority and AISlowTurretPriority on items and characters. Setting the priority to 0 can be used for telling the bots to ignore the target entirely. Items also need to have IsAITurretTarget="True" enabled to make them a valid target. +- Added ItemDamageMultiplier on items. Can be used for increasing the damage caused by other items, like weapons. Works like the existing ExplosionDamageMultiplier. --------------------------------------------------------------------------------------------------------- -v0.21.0.0 (unstable) +v0.21.6.0 --------------------------------------------------------------------------------------------------------- +- Minor localization fixes. +- Fixed some occasional crashes in the character editor. + + +--------------------------------------------------------------------------------------------------------- +v0.21.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed delete save button not working in the singleplayer "load game" menu. +- Fixed depleted fuel revolver round recipe requiring too many materials. +- Reduced medical item cooldown to 0.5 seconds. +- Minor fixes to Chinese localization. +- Fix bugged sufforing poisoning when it reaches the full strength. +- Fixed crashing when you save a sub that contains item variants with decorative sprites (most wrecks) as a new content package. + +--------------------------------------------------------------------------------------------------------- +v0.21.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when an item spawns after upgrades with material requirements have been purchased in the multiplayer campaign. +- Fixed crashing if the submarine preview is closed before generating the preview finishes. + Changes: +- Some submarine upgrades cost materials in addition to money. +- Crush depths are displayed on the submarine switch terminal. +- Miscellaneous fixes to all translations. - The "max missions" campaign setting is restricted to a maximum of 3. +- Minor improvements to ragdoll animation when falling prone due to stun/unconsciousness/ragdolling. - Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills. +- Changed Alarm Buzzer sound to differentiate it from the diving suit warning beep. +- Depth charge tweaks: require UEX, increase non-contained item explosion radius and damage, allow quality to affect explosion, increase pricing. +- Reduced Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. +- Lights flicker when hit by EMP. +- Large monsters no longer drop the loot when they die. This was implemented as a workaround to it being difficult to grab large monsters, but now they're much easier to grab - you can grab them anywhere near their main limb, instead of having to find the "origin" somewhere at the center of the monster. +- Bots now prefer PUCS over the other diving suits and the other diving suits over exosuit. +- Made drinks and other consumables work more consistently. +- Adjusted the aim poses used while holding certain melee weapons. +- Harpoons: Adjusted the damage on walls and the projectile launch forces. Spears can now stick to the walls. Inversed the reeling logic: ragdoll key = reel in. + +Poison overhaul: +- Reworked most poison afflictions with the goal that a medic can now use them offensively on monsters and humans alike. +- Sufforin, Morbusine, Cyanide, Paralyzant now work on most monsters. Bigger monsters are generally more resistant than the smaller. +- Sufforin poisoning: Slowly makes the target fall sick. Eventually leads to death. Stronger monsters may recover. +- Morbusine poisoning: Kills the target relatively quickly from gradual oxygen deprivation/organ damage. Stronger monsters may recover. +- Cyanide poisoning: Doesn't do much at first, but progresses rapidly and kills the target suddenly. Lethal even to the strongest monsters. +- Paralysis: Advances more quickly than previously. Effective even on larger monsters. (Leucocytes don't always apply enough paralysis for it to progress. They also use a version of the affliction that progresses using the old, slower speed.) +- Poisons (and paralysis) now wear off when the affliction level is low. +- Made medic AI react sooner to treat poisons. +- Attempts to poison the target can now fail when the user has a low medical skill. +- Added a short cooldown to applying medical items. Prevents being able to spam medicine (or poisons) at a nearly unlimited rate. +- Adjusted the syringe and stun gun dart trajectories. +- Reworked the weapons and the medical skill gains: instead of always increasing the weapons skill when a target takes damage, afflictions like poisons now increase the medical skill. Also applying the buffs now increases the medical skill. The skill increases are defined per affliction. +- Fixed the skill gains on low levels (0-15) being ridiculously high. + +Talents: +- Fixed "Down With the Ship" sometimes having an incorrect description. +- Fixed "Bounty Hunter" and "Logistics Expert" not giving 15% experience bonus. +- Pet raptor eggs can be fabricated from raptor eggs of any size. +- Fixed Flamer being craftable by anyone, even though it should only be possible with the "Pyromaniac" talent. +- Fixed ability to move in an unpowered exosuit when you've got any speed buff. +- Fixed "True Potential" and "Chonky Honks" not requiring clown power to work. +- Reduced the chance of finding genetic materials with the "Gene Harvester" talent, fixed genetic materials sometimes being found on defense bots. +- Fixed "Spec Ops" not working properly with shotguns and other weapons that fire multiple projectiles per shot. Only the first projectile that hit did double damage. +- Fixed some parts of Hemulen and Venture not flooding properly. +- "Mass production" talent only applies to items in the "material" category. Fixes e.g. recycle recipes sometimes allowing you to keep the original item. +- Fixed "Lone Wolf" not giving any damage/stun resistance. +- Fixed "Scrap Savant" having a 80% chance of finding scrap instead of 20%. +- Fixed "Steady Tune" not doing what the description says (giving a constantly diminishing 7.5% resistance instead of 60%), made the talent give 100% immunity instead. +- Fixed "Multifunctional" talent not giving a boost to crowbar damage. +- Fixed inaccurate "Unstoppable Curiosity" and "Ph.D in Nuclear Physics" descriptions. +- Fixed skill boost from "Journeyman" not matching the value in the description. +- Fixed "Quickdraw" damage bonus. +- Fixed inaccurate "Journeyman" description. +- Fixed "Oiled Machinery" not increasing fabrication speed. +- Fixed tinkered doors requiring a higher condition percentage to become repaired. +- Fixed "Drunken Sailor" not protecting entirely from the negative effects of drunkness. + +Multiplayer: +- Better support for playing the MP campaign without a host or someone with campaign management permissions on the server: + - You can vote to end the round in the campaign as well. + - Automatic restart works in the campaign mode as well. + - Anyone can manage salaries if there's no-one allowed to do it. + - If there's no host or anyone with permissions in the server, anyone is able to setup a new campaign. + - Campaign can be voted for when game mode voting is enabled. +- Made ragdoll syncing more robust: reduces cases of teleportation/desync when manually ragdolling the character in multiplayer. +- Fixed rejoining clients not regaining control of their character even if the character is still alive, if the client's IP address has changed. A respawn would also not trigger. +- Fixed "failed to write a network message, too much data is being stored in SegmentTable" errors that could occur in various situations: for example when the host has a large ban list, lots of submarines, and when rewiring a device with lots of connections. +- Fixed "Create new character" button not appearing in the tab menu when dead or spectating. +- Fixed clients not entering the server lobby if they accept a server invite during a single player round or tutorial. +- Fixed inability to join IPv4 servers when IPv6 is disabled. +- Fixed hidden submarine list sometimes desyncing if you have specific custom submarines. +- Fixed characters spawning with their inventory, skills, etc intact if they die during a round and the round ends when the client is no longer in the server. Or, to put it another way, spawning as if they hadn't died at all. +- Fixed an issue that prevented some dedicated servers from appearing in the server list. +- Fixed chat-linked wifi components not receiving order and report messages. +- Save the server settings after starting up the server to create the default settings file if it doesn't exist, instead of only when the server is shut down or the settings changed. +- Fixed list of hidden submarines sometimes desyncing. +- Fixed inability to start Linux dedicated server using LinuxGSM due to an incorrect EOL character in DedicatedServer.exe. Bugfixes: - Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting. +- Fixed multiple issues in the tutorials: missing text, events not progressing when not following tutorial's steps, infographics usability issues. - Fixed attacking with a melee weapon making you unable to turn (flip) for a while. +- Fixed ragdolling affecting the character's velocity, allowing it to be used as a way to avoid fall damage. - Hull fixes to vanilla subs and wrecks. - Fixed alien flares practically never spawning in ruins. - Fixed status effects defined inside an attack definition still using the old OnUse/OnFailure logic instead of OnSuccess/OnFailure. +- Fixed "save as item assembly" and "snap to grid" buttons taking cursor focus in the sub editor even when they're not visible. +- Fixed inability to launch custom dedicated server executables from the main menu on mac and linux. +- Fixed inability to drag and drop items from the entity list to small containers (such as battery charging docks) in the sub editor. +- Fixed item condition not decreasing client-side if the condition decreases very slowly: for example when using a thorium rod with the "Cruisin'" talent. +- Fixed flares still emitting light after running out. +- Fixed Electrical Discharge Coil preview not working in the sub editor. +- Fixed alien flares not activating when clicking LMB. +- Fixed crawler's arms getting broken when the character flips (turns) in water. +- Fixed the recycle recipe of flak explosive ammo. +- Fixed misaligned shells in wrecked railgun shell rack. +- Fixed misaligned light component light sprite. +- Fixed crashing if the select audio device is disconnected while in the initial loading screen. +- Fixed inability to sell genetic materials that aren't 100% refined. +- Fixed liquid oxygenite exploding too easily. +- Fixed Hemulen's bottom airlock never fully draining. +- Fixed incorrect damage values in "Genetic Material (Hunter)" tooltip. +- Fixed exosuit not playing the warning beep when low or out of oxygen. +- Fixed "taste test" event showing for everyone and not progressing past the 1st prompt. +- Fixed "special training required" and "too many of this item" messages being shown to everyone when someone tries to place a portable pump. +- Added a pump to RemoraDrone's engine room, otherwise it's impossible to drain. +- Fixed some lighting sprites appearing dimmer than they should (most notably, junction boxes and supercapacitors). +- Fixed submarine's crush depth being displayed incorrectly on the campaign map when a submarine switch is pending: the hull upgrades of the current sub weren't taken into account, even though they carry over to the new sub. +- Fixed item assembly descriptions not showing up in the sub editor unless they're configured in a text file. +- Fixed rapid fissile accelerator ammo causing an explosion when launched, instead of when it hits something. +- Fixed bots sometimes trying to contain multiple ammo boxes when reloading turrets (also affected other items). +- Fixed "Manually Outfitted" not doing anything when starting a campaign. +- Fixed guardians trying to heal themselves in inactive pods (not destroyed pods, but ones deactivated via wiring). +- Fixed bots having trouble with fixing Barsuk's top hatch and leaks around it. +- Fixed sprite bleed in turret icons. +- Fixed radio channel presets resetting between rounds. +- Fixed some lines in the "shockjock" event being shown to everyone. +- Fixed radio noise playing even if you don't have a headset. +- Fixed hitscan projectiles going through subs when you're firing from inside another sub. +- Fixed lights set to flicker/pulse eventually getting out of sync even if they're set to the same frequency. +- Fixes to submarine preview: wires are now visible, scaled doors work correctly, camera is now correctly centered on the sub when opening the preview. +- Fixed the affliction type of some afflictions (like drunk) being "poison", causing them to be treated as poisons. They are now "debuffs" instead. +- Fixed defense bots not attacking pirates or bandits. Also fixed them not protecting the owner when attacked by the outpost guards. +- Fixed bots not knowing how to handle diving suits that don't have an oxygen tank inside them. Now they should be able to use them and refill them with an oxygen tank. Modding: +- Added a button for updating core mods into the Mods menu. - Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases: - Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to. - - Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon, Projectile, and Throwable components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons. + - Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon and Projectile components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons. - MODDERS, PLEASE NOTE: Effects that target "Character" in the previously mentioned components now affect the user - if that's not the intention, the target should be changed to "UseTarget". + - The use target can now be a character, an item, or a structure, depending on the context. This allows effects that weren't previously possible, but due to it we'll now need to introduce some restrictions in the definitions in some cases. For example, we might want to use a conditional to check whether the target is of the right type, before triggering the status effect (). + - Added a new attribute for the MeleeWeapon component, "HitOnlyCharacters", which can be used for ignoring the hits to walls and items entirely. + - Due to the changes, some status effects that previously worked, might now need the "AllowWhenBroken" set to true in the definition to keep them working as they used to. So e.g. the "OnImpact" doesn't work anymore on your custom explosive, try that. +- Fixed crashing when trying to place a wreck with no hulls in a level. +- Fixed mod descriptions getting truncated to 255 characters when selecting an already-published item in the Mods menu. +- Fixed HMG's requiring hmgmagazine instead of any item with the type "hmgammo", making the use of modded ammo impossible without overriding the gun too. +- Fixed crashing when an upgrade tries to increase an integer value. +- Changed the affliction type of some afflictions, which might have implications on mods if they targeted them by type. +- Added AimAngle on Holdable item components. Can be used for defining a different hold angle for the aim and the rest poses. Note that on Holdables, the angles are mutually exclusive (defined separately), but on MeleeWeapons they are cumulative (added together). Therefore, no need to change the existing items! +- Fixed definitions not triggering the status effects that have the target type "UseTarget". --------------------------------------------------------------------------------------------------------- v0.20.16.1 diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs index 9ca64ea88..28c80c246 100644 --- a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs @@ -7,22 +7,25 @@ namespace Steamworks public byte[] Data; public uint Handle; - /// - /// Cancels a ticket. - /// You should cancel your ticket when you close the game or leave a server. - /// - public void Cancel() - { - if ( Handle != 0 ) - { - SteamUser.Internal.CancelAuthTicket( Handle ); - } + public bool Canceled { get; private set; } - Handle = 0; - Data = null; - } + /// + /// Cancels a ticket. + /// You should cancel your ticket when you close the game or leave a server. + /// + public void Cancel() + { + if (Handle != 0) + { + SteamUser.Internal.CancelAuthTicket(Handle); + } - public void Dispose() + Handle = 0; + Data = null; + Canceled = true; + } + + public void Dispose() { Cancel(); } diff --git a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs index ea0bbd412..9f0feeaf2 100644 --- a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs +++ b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs @@ -2,7 +2,7 @@ The MIT License (MIT) -Copyright (c) 2013-2019 Riley Labrecque +Copyright (c) 2013-2022 Riley Labrecque Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,48 +25,60 @@ THE SOFTWARE. **/ using System; -using System.Net; using System.Runtime.InteropServices; namespace Steamworks { //----------------------------------------------------------------------------- - // Purpose: Callback interface for receiving responses after pinging an individual server + // Purpose: Callback interface for receiving responses after requesting rules + // details on a particular server. // // These callbacks all occur in response to querying an individual server - // via the ISteamMatchmakingServers()->PingServer() call below. If you are - // destructing an object that implements this interface then you should call + // via the ISteamMatchmakingServers()->ServerRules() call below. If you are + // destructing an object that implements this interface then you should call // ISteamMatchmakingServers()->CancelServerQuery() passing in the handle to the query // which is in progress. Failure to cancel in progress queries when destructing // a callback handler may result in a crash when a callback later occurs. //----------------------------------------------------------------------------- - public class SteamMatchmakingPingResponse + public class SteamMatchmakingRulesResponse { - // Server has responded successfully and has updated data - public delegate void ServerResponded(Steamworks.Data.ServerInfo server); + // Got data on a rule on the server -- you'll get one of these per rule defined on + // the server you are querying + public delegate void RulesResponded(string pchRule, string pchValue); - // Server failed to respond to the ping request - public delegate void ServerFailedToRespond(); + // The server failed to respond to the request for rule details + public delegate void RulesFailedToRespond(); - private VTable m_VTable; - private IntPtr m_pVTable; + // The server has finished responding to the rule details request + // (ie, you won't get anymore RulesResponded callbacks) + public delegate void RulesRefreshComplete(); + + private readonly VTable m_VTable; + private readonly IntPtr m_pVTable; private GCHandle m_pGCHandle; - private ServerResponded m_ServerResponded; - private ServerFailedToRespond m_ServerFailedToRespond; + private readonly RulesResponded m_RulesResponded; + private readonly RulesFailedToRespond m_RulesFailedToRespond; + private readonly RulesRefreshComplete m_RulesRefreshComplete; - public SteamMatchmakingPingResponse(ServerResponded onServerResponded, ServerFailedToRespond onServerFailedToRespond) + public SteamMatchmakingRulesResponse( + RulesResponded onRulesResponded, + RulesFailedToRespond onRulesFailedToRespond, + RulesRefreshComplete onRulesRefreshComplete) { - if (onServerResponded == null || onServerFailedToRespond == null) + if (onRulesResponded == null || onRulesFailedToRespond == null || onRulesRefreshComplete == null) { throw new ArgumentNullException(); } - m_ServerResponded = onServerResponded; - m_ServerFailedToRespond = onServerFailedToRespond; + + m_RulesResponded = onRulesResponded; + m_RulesFailedToRespond = onRulesFailedToRespond; + m_RulesRefreshComplete = onRulesRefreshComplete; m_VTable = new VTable() { - m_VTServerResponded = InternalOnServerResponded, - m_VTServerFailedToRespond = InternalOnServerFailedToRespond, + m_VTRulesResponded = InternalOnRulesResponded, + m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, + m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete }; m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); Marshal.StructureToPtr(m_VTable, m_pVTable, false); @@ -74,21 +86,7 @@ namespace Steamworks m_pGCHandle = GCHandle.Alloc(m_pVTable, GCHandleType.Pinned); } - private Data.HServerQuery hserverPing = 0; - public bool QueryActive { get { return hserverPing != 0; } } - - public void Cancel() - { - if (hserverPing != 0) { ServerList.Base.Internal.CancelServerQuery(hserverPing); } - hserverPing = 0; - } - - public void HQueryPing(IPAddress ip, int port) - { - hserverPing = ServerList.Base.Internal.PingServer(ip.IpToInt32(), (ushort)port, (IntPtr)this); - } - - ~SteamMatchmakingPingResponse() + ~SteamMatchmakingRulesResponse() { if (m_pVTable != IntPtr.Zero) { @@ -102,48 +100,69 @@ namespace Steamworks } #if NOTHISPTR - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate void InternalServerResponded(gameserveritem_t server); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate void InternalServerFailedToRespond(); - private void InternalOnServerResponded(gameserveritem_t server) { - m_ServerResponded(server); - } - private void InternalOnServerFailedToRespond() { - m_ServerFailedToRespond(); - } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesResponded(IntPtr pchRule, IntPtr pchValue); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesFailedToRespond(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesRefreshComplete(); + + private void InternalOnRulesResponded(IntPtr pchRule, IntPtr pchValue) + { + m_RulesResponded(Helpers.MemoryToString(pchRule), Helpers.MemoryToString(pchValue)); + } + + private void InternalOnRulesFailedToRespond() + { + m_RulesFailedToRespond(); + } + + private void InternalOnRulesRefreshComplete() + { + m_RulesRefreshComplete(); + } #else [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate void InternalServerResponded(IntPtr thisptr, Steamworks.Data.gameserveritem_t server); + public delegate void InternalRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate void InternalServerFailedToRespond(IntPtr thisptr); - private void InternalOnServerResponded(IntPtr thisptr, Steamworks.Data.gameserveritem_t server) - { - hserverPing = 0; + public delegate void InternalRulesFailedToRespond(IntPtr thisptr); - m_ServerResponded(Steamworks.Data.ServerInfo.From(server)); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalRulesRefreshComplete(IntPtr thisptr); + + private void InternalOnRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue) + { + m_RulesResponded(Helpers.MemoryToString(pchRule), Helpers.MemoryToString(pchValue)); } - private void InternalOnServerFailedToRespond(IntPtr thisptr) - { - hserverPing = 0; - m_ServerFailedToRespond(); + private void InternalOnRulesFailedToRespond(IntPtr thisptr) + { + m_RulesFailedToRespond(); + } + + private void InternalOnRulesRefreshComplete(IntPtr thisptr) + { + m_RulesRefreshComplete(); } #endif [StructLayout(LayoutKind.Sequential)] private class VTable { - [NonSerialized] - [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalServerResponded m_VTServerResponded; + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesResponded m_VTRulesResponded; - [NonSerialized] - [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalServerFailedToRespond m_VTServerFailedToRespond; + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesRefreshComplete m_VTRulesRefreshComplete; } - public static explicit operator System.IntPtr(SteamMatchmakingPingResponse that) + public static explicit operator System.IntPtr(SteamMatchmakingRulesResponse that) { return that.m_pGCHandle.AddrOfPinnedObject(); } diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index e2f02d7f0..bc1c4d848 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -1,209 +1,76 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Threading.Tasks; -using Steamworks.Data; namespace Steamworks { internal static class SourceServerQuery { - private static readonly byte[] A2S_SERVERQUERY_GETCHALLENGE = { 0x55, 0xFF, 0xFF, 0xFF, 0xFF }; - // private static readonly byte A2S_PLAYER = 0x55; - private const byte A2S_RULES = 0x56; - - private static readonly Dictionary>> PendingQueries = - new Dictionary>>(); - - private static HashSet activeRequests = new HashSet(); - private static int lastRequestId = 0; - - internal static Task> GetRules( ServerInfo server ) - { - var endpoint = new IPEndPoint(server.Address, server.QueryPort); - - lock (PendingQueries) - { - if (PendingQueries.TryGetValue(endpoint, out var pending)) - return pending; - - var task = GetRulesImpl( endpoint ) - .ContinueWith(t => - { - lock (PendingQueries) - { - PendingQueries.Remove(endpoint); - } - - return t; - }) - .Unwrap(); - - PendingQueries.Add(endpoint, task); - return task; - } - } - - private static async Task> GetRulesImpl( IPEndPoint endpoint ) - { - int currId; - lock (activeRequests) - { - lastRequestId++; - currId = lastRequestId; - activeRequests.Add(currId); - } - - try - { - await Task.Yield(); - while (true) - { - lock (activeRequests) - { - if (!activeRequests.Any() || (currId - activeRequests.Min()) < 25) { break; } - } - await Task.Delay(25); - } - - using (var client = new UdpClient()) - { - client.Client.SendTimeout = 3000; - client.Client.ReceiveTimeout = 3000; - client.Connect(endpoint); - - return await GetRules(client); - } - } - catch (System.Exception) - { - //Console.Error.WriteLine( e.Message ); - return null; - } - finally - { - lock (activeRequests) - { - activeRequests.Remove(currId); - } - } + private enum Status + { + Pending, + Failure, + Success } - static async Task> GetRules( UdpClient client ) + private static readonly HashSet ruleResponseHandlers + = new HashSet(); + + internal static async Task> GetRules(Steamworks.Data.ServerInfo server) { - var challengeBytes = await GetChallengeData( client ); - challengeBytes[0] = A2S_RULES; - await Send( client, challengeBytes ); - var ruleData = await Receive( client ); + Status status = Status.Pending; var rules = new Dictionary(); - using ( var br = new BinaryReader( new MemoryStream( ruleData ) ) ) - { - if ( br.ReadByte() != 0x45 ) - throw new Exception( "Invalid data received in response to A2S_RULES request" ); + SteamMatchmakingRulesResponse responseHandler = null; - var numRules = br.ReadUInt16(); - for ( int index = 0; index < numRules; index++ ) - { - rules.Add( br.ReadNullTerminatedUTF8String(), br.ReadNullTerminatedUTF8String() ); - } + void onRulesResponded(string key, string value) + => rules.Add(key, value); + + void onRulesFailToRespond() + { + finish(Status.Failure); } - return rules; - } - - - - static async Task Receive( UdpClient client ) - { - byte[][] packets = null; - byte packetNumber = 0, packetCount = 1; - - do + void onRulesRefreshComplete() { - Task result = client.ReceiveAsync(); - await Task.WhenAny(result, Task.Delay(3000)); - if (!result.IsCompleted) - { - throw new Exception("Receive timed out"); - } - var buffer = result.Result.Buffer; - - using ( var br = new BinaryReader( new MemoryStream( buffer ) ) ) - { - var header = br.ReadInt32(); - - if ( header == -1 ) - { - var unsplitdata = new byte[buffer.Length - br.BaseStream.Position]; - Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, unsplitdata, 0, unsplitdata.Length ); - return unsplitdata; - } - else if ( header == -2 ) - { - int requestId = br.ReadInt32(); - packetNumber = br.ReadByte(); - packetCount = br.ReadByte(); - int splitSize = br.ReadInt32(); - } - else - { - throw new System.Exception( "Invalid Header" ); - } - - if ( packets == null ) packets = new byte[packetCount][]; - - var data = new byte[buffer.Length - br.BaseStream.Position]; - Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, data, 0, data.Length ); - packets[packetNumber] = data; - } + finish(Status.Success); } - while ( packets.Any( p => p == null ) ); - var combinedData = Combine( packets ); - return combinedData; - } - - private static async Task GetChallengeData( UdpClient client ) - { - await Send( client, A2S_SERVERQUERY_GETCHALLENGE ); - - var challengeData = await Receive( client ); - - if ( challengeData[0] != 0x41 ) - throw new Exception( "Invalid Challenge" ); - - return challengeData; - } - - static async Task Send( UdpClient client, byte[] message ) - { - var sendBuffer = new byte[message.Length + 4]; - - sendBuffer[0] = 0xFF; - sendBuffer[1] = 0xFF; - sendBuffer[2] = 0xFF; - sendBuffer[3] = 0xFF; - - Buffer.BlockCopy( message, 0, sendBuffer, 4, message.Length ); - - await client.SendAsync( sendBuffer, message.Length + 4 ); - } - - static byte[] Combine( byte[][] arrays ) - { - var rv = new byte[arrays.Sum( a => a.Length )]; - int offset = 0; - foreach ( byte[] array in arrays ) + void finish(Status stat) { - Buffer.BlockCopy( array, 0, rv, offset, array.Length ); - offset += array.Length; + if (status == Status.Pending) { status = stat; } + + var handler = responseHandler; + if (handler is null) { return; } + + lock (ruleResponseHandlers) + { + ruleResponseHandlers.Remove(handler); + } + responseHandler = null; } - return rv; + + responseHandler = new SteamMatchmakingRulesResponse( + onRulesResponded, + onRulesFailToRespond, + onRulesRefreshComplete); + lock (ruleResponseHandlers) + { + ruleResponseHandlers.Add(responseHandler); + } + + var query = SteamMatchmakingServers.Internal.ServerRules( + server.AddressRaw, (ushort)server.QueryPort, (IntPtr)responseHandler); + + while (status == Status.Pending) + { + await Task.Delay(25); + } + + SteamMatchmakingServers.Internal.CancelServerQuery(query); + + return status == Status.Success ? rules : null; } }; diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index d2765ade2..bddd50da5 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -124,7 +124,24 @@ namespace Lidgren.Network m_lastSocketBind = now; if (m_socket == null) - m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + { + try + { + m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + } + catch (SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.AddressFamilyNotSupported) + { + // Fall back to IPv4 if IPv6 is unsupported + m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + } + else + { + throw; + } + } + } if (reBind) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); @@ -132,9 +149,9 @@ namespace Lidgren.Network m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - m_socket.DualMode = m_configuration.UseDualModeSockets; + if (m_socket.AddressFamily == AddressFamily.InterNetworkV6) { m_socket.DualMode = m_configuration.UseDualModeSockets; } - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port); + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToFamily(m_socket.AddressFamily), reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); try @@ -413,6 +430,10 @@ namespace Lidgren.Network int bytesReceived = 0; try { + if (m_senderRemote is IPEndPoint ipEndpoint && ipEndpoint.AddressFamily != m_socket.AddressFamily) + { + m_senderRemote = ipEndpoint.MapToFamily(m_socket.AddressFamily); + } bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); } catch (SocketException sx) diff --git a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs index b49585afa..8a255010e 100644 --- a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -136,7 +136,7 @@ namespace Lidgren.Network { connectionReset = false; - target = NetUtility.MapToIPv6(target); + target = target.MapToFamily(m_socket.AddressFamily); IPAddress ba = default(IPAddress); try diff --git a/Libraries/Lidgren.Network/NetPeer.cs b/Libraries/Lidgren.Network/NetPeer.cs index eafef5d7c..538a87996 100644 --- a/Libraries/Lidgren.Network/NetPeer.cs +++ b/Libraries/Lidgren.Network/NetPeer.cs @@ -293,7 +293,7 @@ namespace Lidgren.Network if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); - remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); + remoteEndPoint = remoteEndPoint.MapToFamily(m_socket.AddressFamily); lock (m_connections) { diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index 0c437ce7f..cf5996e98 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -454,16 +454,24 @@ namespace Lidgren.Network return ComputeSHAHash(bytes, 0, bytes.Length); } - /// - /// Maps the IPEndPoint object to an IPv6 address, if it is currently mapped to an IPv4 address. - /// - internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) - { - if (endPoint.AddressFamily == AddressFamily.InterNetwork) - { - return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); - } - return endPoint; - } + internal static IPAddress MapToFamily(this IPAddress address, AddressFamily family) + { + switch (family) + { + case AddressFamily.InterNetworkV6: + return address.MapToIPv6(); + case AddressFamily.InterNetwork: + return address.MapToIPv4(); + default: + throw new Exception($"Unsupported address family: {family}"); + } + } + + internal static IPEndPoint MapToFamily(this IPEndPoint endpoint, AddressFamily family) + { + return endpoint.Address.AddressFamily == family + ? endpoint + : new IPEndPoint(endpoint.Address.MapToFamily(family), endpoint.Port); + } } } \ No newline at end of file diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index 441214b6e..2d08ff6ec 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -12,15 +12,22 @@ namespace Microsoft.Xna.Framework.Graphics { public interface ISpriteBatch { - public void Draw(Texture2D texture, - Vector2 position, - Rectangle? sourceRectangle, - Color color, - float rotation, - Vector2 origin, - Vector2 scale, - SpriteEffects effects, - float layerDepth); + public void Draw( + Texture2D texture, + Vector2 position, + Rectangle? sourceRectangle, + Color color, + float rotation, + Vector2 origin, + Vector2 scale, + SpriteEffects effects, + float layerDepth); + + public void Draw( + Texture2D texture, + VertexPositionColorTexture[] vertices, + float layerDepth, + int? count = null); } ///