From 25fa5a9552f114e7023a8cbccacf24e7d6ab8705 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Tue, 31 Jan 2023 18:01:29 +0200 Subject: [PATCH] Build 0.21.6.0 --- .../ClientSource/CameraTransition.cs | 14 +- .../ClientSource/Characters/CharacterHUD.cs | 2 + .../Characters/Health/CharacterHealth.cs | 2 + .../Health/HealingCooldownClient.cs | 22 ++ .../ClientSource/Characters/Limb.cs | 181 +++++++++--- .../ContentPackage/ModProject.cs | 8 +- .../ContentPackageManager.cs | 2 +- .../ClientSource/DebugConsole.cs | 24 ++ .../EventActions/CheckObjectiveAction.cs | 8 +- .../Events/EventActions/UIHighlightAction.cs | 30 +- .../ClientSource/GUI/GUIComponent.cs | 5 +- .../ClientSource/GUI/GUIMessageBox.cs | 3 +- .../ClientSource/GUI/GUIStyle.cs | 1 + .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 5 +- .../ClientSource/GUI/UpgradeStore.cs | 2 +- .../BarotraumaClient/ClientSource/GameMain.cs | 14 +- .../ClientSource/GameSession/CrewManager.cs | 10 +- .../ClientSource/Items/CharacterInventory.cs | 18 +- .../ClientSource/Items/Components/Door.cs | 58 ++-- .../Items/Components/LightComponent.cs | 11 +- .../Items/Components/Machines/MiniMap.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 13 +- .../Components/Signal/CustomInterface.cs | 1 + .../ClientSource/Items/Components/Wearable.cs | 5 +- .../ClientSource/Items/Inventory.cs | 9 + .../ClientSource/Items/Item.cs | 10 +- .../ClientSource/Items/ItemPrefab.cs | 10 + .../ClientSource/Map/Levels/WaterRenderer.cs | 11 +- .../ClientSource/Map/Lights/LightManager.cs | 18 +- .../ClientSource/Map/Lights/LightSource.cs | 13 +- .../ClientSource/Map/Map/Map.cs | 43 +-- .../ClientSource/Map/SubmarineInfo.cs | 74 ++++- .../ClientSource/Map/SubmarinePreview.cs | 7 +- .../ClientSource/Networking/BanList.cs | 11 +- .../ClientSource/Networking/ChatMessage.cs | 7 +- .../Networking/Primitives/Peers/ClientPeer.cs | 5 + .../Networking/Voip/VoipClient.cs | 2 +- .../CampaignSetupUI/CampaignSetupUI.cs | 48 +++- .../MultiPlayerCampaignSetupUI.cs | 58 +--- .../SinglePlayerCampaignSetupUI.cs | 110 +++----- .../ClientSource/Screens/CampaignUI.cs | 1 + .../CharacterEditor/CharacterEditorScreen.cs | 28 +- .../ClientSource/Screens/GameScreen.cs | 22 +- .../ClientSource/Screens/MainMenuScreen.cs | 2 +- .../ClientSource/Screens/TestScreen.cs | 3 +- .../Serialization/SerializableEntityEditor.cs | 14 +- .../ClientSource/Sprite/DeformableSprite.cs | 7 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 183 ++++++------ .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 7 +- .../ClientSource/Utils/EffectLoader.cs | 12 + .../ClientSource/Utils/SpriteRecorder.cs | 4 +- .../Content/Effects/wearableclip.xnb | Bin 0 -> 2056 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 0 -> 1842 bytes .../BarotraumaClient/GlobalSuppressions.cs | 2 + .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/Shaders/Content.mgcb | 5 + .../Shaders/Content_opengl.mgcb | 6 + .../BarotraumaClient/Shaders/wearableclip.fx | 42 +++ .../Shaders/wearableclip_opengl.fx | 42 +++ .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 7 + .../Health/HealingCooldownServer.cs | 51 ++++ .../GameModes/CharacterCampaignData.cs | 11 +- .../GameModes/MultiPlayerCampaign.cs | 16 +- .../ServerSource/Items/Inventory.cs | 19 +- .../ServerSource/Items/Item.cs | 16 +- .../ServerSource/Networking/BanList.cs | 53 ++-- .../ServerSource/Networking/GameServer.cs | 17 +- .../Networking/OrderChatMessage.cs | 1 + .../Primitives/Peers/Server/ServerPeer.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 15 - .../Characters/AI/EnemyAIController.cs | 37 ++- .../Characters/AI/HumanAIController.cs | 50 ++-- .../AI/Objectives/AIObjectiveCombat.cs | 16 +- .../AI/Objectives/AIObjectiveContainItem.cs | 3 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 13 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 10 +- .../AI/Objectives/AIObjectiveGetItem.cs | 10 +- .../AI/Objectives/AIObjectiveGetItems.cs | 4 +- .../AI/Objectives/AIObjectiveManager.cs | 13 +- .../AI/Objectives/AIObjectivePrepare.cs | 7 +- .../AI/Objectives/AIObjectiveRescue.cs | 18 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 25 +- .../Animation/HumanoidAnimController.cs | 2 +- .../SharedSource/Characters/Character.cs | 137 +++++++-- .../Health/Afflictions/AfflictionPrefab.cs | 29 +- .../Characters/Health/CharacterHealth.cs | 2 +- .../SharedSource/Characters/Limb.cs | 17 +- .../Characters/Params/CharacterParams.cs | 5 +- .../ContentPackage/ContentPackage.cs | 9 +- .../SharedSource/DebugConsole.cs | 1 + .../Events/EventActions/UIHighlightAction.cs | 4 +- .../GameSession/AutoItemPlacer.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 14 +- .../SharedSource/GameSession/GameSession.cs | 9 +- .../GameSession/UpgradeManager.cs | 14 +- .../SharedSource/Items/CharacterInventory.cs | 5 +- .../Items/Components/DockingPort.cs | 11 +- .../SharedSource/Items/Components/Door.cs | 10 +- .../Items/Components/Holdable/Holdable.cs | 39 ++- .../Items/Components/Holdable/MeleeWeapon.cs | 6 +- .../Items/Components/Holdable/Pickable.cs | 2 + .../Items/Components/Holdable/Throwable.cs | 4 +- .../Items/Components/ItemComponent.cs | 11 +- .../Items/Components/ItemContainer.cs | 11 +- .../Items/Components/Projectile.cs | 50 ++-- .../SharedSource/Items/Components/Rope.cs | 41 +-- .../Items/Components/Signal/LightComponent.cs | 8 + .../Items/Components/Signal/WifiComponent.cs | 14 +- .../SharedSource/Items/Components/Wearable.cs | 24 +- .../SharedSource/Items/Inventory.cs | 2 + .../SharedSource/Items/Item.cs | 56 ++-- .../SharedSource/Items/ItemPrefab.cs | 15 +- .../SharedSource/Map/Entity.cs | 26 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 6 + .../SharedSource/Map/Levels/LevelData.cs | 18 +- .../SharedSource/Map/Map/Location.cs | 7 +- .../SharedSource/Map/SubmarineInfo.cs | 12 +- .../SharedSource/Networking/BanList.cs | 2 +- .../Networking/INetSerializableStruct.cs | 38 ++- .../Primitives/NetworkPeerStructs.cs | 2 +- .../Serialization/XMLExtensions.cs | 44 ++- .../StatusEffects/StatusEffect.cs | 68 ++--- .../SharedSource/Steam/Workshop.cs | 3 +- .../SharedSource/SteamAchievementManager.cs | 2 + .../SharedSource/Upgrades/Upgrade.cs | 2 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 2 +- .../SharedSource/Utils/CoordinateSpace2D.cs | 26 ++ .../SharedSource/Utils/MathUtils.cs | 3 + .../SharedSource/Utils/SaveUtil.cs | 22 +- .../Utils/SerializableDateTime.cs | 266 ++++++++++++++++++ .../SharedSource/Utils/ToolBox.cs | 43 --- Barotrauma/BarotraumaShared/changelog.txt | 95 ++++++- .../BarotraumaTest/CoordinateSpace2DTests.cs | 55 ++++ ...tSerializableStructImplementationChecks.cs | 4 +- .../SerializableDateTimeTests.cs | 64 +++++ Barotrauma/BarotraumaTest/TestProject.cs | 4 +- .../Classes/AuthTicket.cs | 31 +- .../SteamMatchmakingResponses.cs | 143 ++++++---- .../Utility/SourceServerQuery.cs | 235 ++++------------ 145 files changed, 2317 insertions(+), 1145 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb create mode 100644 Barotrauma/BarotraumaClient/Shaders/wearableclip.fx create mode 100644 Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs create mode 100644 Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs create mode 100644 Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index ed7ff67d7..b1392f9cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -154,9 +154,12 @@ 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; @@ -170,8 +173,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/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index d0b25b398..80b153e28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -724,6 +724,8 @@ namespace Barotrauma } bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; + Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color; if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d9801606c..6fe86291d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2071,6 +2071,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) { 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 4a8c36df7..5fe12e73e 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 == "damage") { damage += affliction.GetVitalityDecrease(null); } @@ -732,7 +731,7 @@ namespace Barotrauma 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 2833cf1d1..19d98e5e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1128,6 +1128,28 @@ namespace Barotrauma }); 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)) @@ -1229,12 +1251,14 @@ 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; 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/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/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 81ad4ac35..fc1ade91e 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/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/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 21481ef4e..ed1740549 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -615,7 +615,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(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index ee9de1b18..75219550f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1779,7 +1779,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 3399c5399..3a2e531c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1171,7 +1171,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.Right) { 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/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 2654387fe..a730310d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -23,9 +23,13 @@ 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; @@ -398,7 +402,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); @@ -512,10 +516,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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5820ca4cd..7a09abd5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -788,7 +788,6 @@ namespace Barotrauma { return; } - if (ws != null) { hull = Hull.FindHull(ws.WorldPosition); @@ -802,7 +801,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); @@ -817,13 +815,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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 11530061e..18139a698 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -965,7 +965,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 +986,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index aa7baf8be..f5faabb79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -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/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/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 85ac7fd01..f901ec9ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -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; } 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/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/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..5b4d60065 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1713,6 +1713,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 && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 9bd922451..684cb0d5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -203,7 +203,7 @@ namespace Barotrauma } } - partial void InitProjSpecific() + public void InitSpriteStates() { Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); @@ -211,7 +211,6 @@ namespace Barotrauma { brokenSprite.Sprite.EnsureLazyLoaded(); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); @@ -221,6 +220,11 @@ namespace Barotrauma UpdateSpriteStates(0.0f); } + partial void InitProjSpecific() + { + InitSpriteStates(); + } + private Rectangle? cachedVisibleExtents; public void ResetCachedVisibleSize() @@ -1409,7 +1413,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..17cd3e848 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() 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/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e3aec5d91..d8314b1b9 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); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 6fe45ed52..7f981e74f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -200,8 +200,6 @@ namespace Barotrauma.Lights private static Texture2D lightTexture; - private float blinkTimer, flickerState, pulseState; - private VertexPositionColorTexture[] vertices; private short[] indices; @@ -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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 8339eb987..bdc94b8de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -68,7 +68,7 @@ namespace Barotrauma private (Rectangle targetArea, RichString tip)? tooltip; - private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + private SubmarineInfo.PendingSubInfo pendingSubInfo; private RichString beaconStationActiveText, beaconStationInactiveText; @@ -936,39 +936,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) { @@ -1125,6 +1094,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/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 a4087003a..b3f830140 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -45,7 +45,7 @@ namespace Barotrauma } } - private struct Door + private readonly struct Door { public readonly Rectangle Rect; @@ -153,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() { @@ -191,6 +193,7 @@ namespace Barotrauma 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 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/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/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 7698c4b66..bf262be14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -120,6 +120,7 @@ namespace Barotrauma.Networking 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 +132,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/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 60dc7f6ea..0bc8adcd3 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; @@ -58,7 +59,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 +88,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,6 +102,26 @@ 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 TutorialEnabled; @@ -367,5 +387,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..a3ae05b14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -581,7 +581,7 @@ namespace Barotrauma } } - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -649,46 +649,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 +682,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 f8450df3b..00591d5b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -551,6 +551,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 78ef8b08a..102992cae 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; } } @@ -262,7 +259,10 @@ namespace Barotrauma.CharacterEditor #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; - GameMain.LightManager.LightingEnabled = true; + if (!GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = true; + } ClearWidgets(); ClearSelection(); } @@ -280,6 +280,7 @@ namespace Barotrauma.CharacterEditor #region Main methods public override void AddToGUIUpdateList() { + if (rightArea == null || leftArea == null) { return; } rightArea.AddToGUIUpdateList(); leftArea.AddToGUIUpdateList(); @@ -778,7 +779,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) @@ -1570,10 +1571,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; } @@ -3998,7 +3996,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { @@ -4107,7 +4105,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/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index dd9aba825..ac0be6534 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.Diagnostics; @@ -27,7 +26,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)); @@ -38,20 +37,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 284a9634a..107c22358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -994,7 +994,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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index e36471206..a5e37dcdb 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/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/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/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/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index f0315b696..f7c0b35fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -9,7 +9,7 @@ namespace Barotrauma { sealed class SpriteRecorder : ISpriteBatch, IDisposable { - private readonly record struct Command( + public readonly record struct Command( Texture2D Texture, VertexPositionColorTexture VertexBL, VertexPositionColorTexture VertexBR, @@ -346,7 +346,7 @@ namespace Barotrauma } recordedBuffers.Clear(); commandList.Clear(); - indexBuffer.Dispose(); indexBuffer = null; + indexBuffer?.Dispose(); indexBuffer = null; ReadyToRender = false; } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb new file mode 100644 index 0000000000000000000000000000000000000000..1f290fc5c1ed740460bf482de9f1bb57fc3dbc8f GIT binary patch literal 2056 zcmbtVO>Y}j6g~6d+6@Y}kU*?pKwVlEX#})Lp-JpGMUce!qjC`vam<)nHJ)f}Hw{8) z1Zz-Ntlb@uO;_w$vW5j>!vZTmAV@4R=e(IwOcdDgq&M&0bI<*F_dU&P?`|Gtkjnwq z@lL-R2BV-iF13f9QZ?*+>>mZ;{Zcs?j{V`dwAJhR-ErOTeCUToZzmWA+oYTmi{2eS z9QA|Y+G^?b(n@LN#?7Kvo(#s5&|e$+lX2J?6usKyV9@Wr?H@OT`~GlkI2jC}VI60I zo$YEnXZ^7H)E^f1;Rr6kuED~mSAa@;v;5oc@2~#4{L4!}ef#z4mw*0sYLO$G%k>q` zZ*1V_3~-y+Mlig2NL>^5d|u>Qk%%I{k}8z1B{FkaiUyO{2*@zAs^FmiyFkW8ehNgl z43>OeR7iqA{z)p+FO?r5m!Bs1Ba1S#kZ(tf{5Y1a$VJQG^ouOao(Q+#633L=aV+GB zLvu>ZjrA6F`OU?1ZCKR5n-Z7DaeieY~yFsPZ+t(XGMT`#+SNH zU9!O#*P%Y6t_jB>UtAiP8@8M1k{$Y{ey-Vf%kVZ$eR&hd%FU+IE#z6tEJ}5ox^kpC z`_G!sb%(l>=u0M^dN8NYJd&<`IFwbpboB_-)svL(8h*S_o}uBA|9Ar+Egz+I>RiA& zq7rI z=$pAtyYf_%9Ktpe%vu*ey2a?ZXNakWs-5t^@MJW;va5#6?7}#*XGzTG+_=^%SJ}3J ztFp0;s3x51XK@boPPL^R$)`(6{(s@{o|`zPmJ6I!)}ei9ypiP8Dj)9q%IArVeH6v? zkc+8Z@xFihziYoNXIi^#s{Jy~VqHrY>RYs_R`np|T(l{6YNL2H-D`?1-VfTbve1u| z6Q4P-4A)7qbPw`L?}^UHjQx1Mp+0#@f6_j^5ZC)AugP1D?OiC~Apuu;$+mGK@Sj&mpxThB!R;Jb& KjaI-P7VtMmb_@Xk literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb new file mode 100644 index 0000000000000000000000000000000000000000..c0ca155d0d16898e275fb4876e175c6835be5e8d GIT binary patch literal 1842 zcmbuA&2G~`5XaX|LrbJcy>MTtmlT5Kk4S(}Bt+Wu07Vr^Q6*GGuDvd++Pl%(&KKeV zka!DTinoE8b^MW}J+TMNJM-K9&&xSYNO#RbJGk zm)UK^{SRz8miJ8bMHWQ_hw=LfAs6Sv>q9d9`G*-t(`1qf9!eD_lNs}S???kNO7<*b zkY7zgj(Hfcke+{>oQ>M8sbW4yc^b2TXYrJVk@V8H4|%vi`!N#%58JJFYb``khi(gh z?cD(EOOd1{09ap7I+kg5Y4xy8>jdaxBW2Ymc75X1CvJVxgUK;u#s$M*d}}u(>t++N zcM*FFadr`B3vqW57m%Vli5E{Jrkqp663pyjk51E5OD^v6g3X2sdT1acsWwxaS+wSZ zvTdvyv&BG46`)eB9w{s;T^k-bu;^Vhf!4j#VtBMO$padDTy##_t?%vDQ;1zz<`T~D zB^K8;7qI=>x&SL0d*-Sz+U5dTu%;HgR_4{bg6D#APrX7^AyLva>5n92~x?64= z8%Dc9E8kNG_yQLz*s*f2BLVk<7Va`646j@cuLV#A4Y{rXj8psT`S!H1c;b61C{A_vgy1U%Lb}Iy)#-C|&hR2) zqvAbV#|4%jc+K|U+f16^ORNSljQrK!H^8{pcs6IA@@^w`H5", Scope = "member", Target = "~M:Barotrauma.GameSettings.CreateSettingsFrame")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "IDE0047")] + diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 7bf8666f0..5c27ba425 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 36467864b..aec4b86f1 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.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 671b920b4..adc6b7153 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 422aaffe1..5ee5b2bc2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index be8a82f6f..6692e0d12 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.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 5aca9fbf7..a8b32422a 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/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/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 089758334..82810135f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -240,9 +240,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 +252,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(); + } } } 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 e04c02d33..3508a03ae 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..c2a1f8cc6 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) @@ -171,14 +187,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 +248,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 +286,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 a12693b51..8c9a90c95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -140,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(); @@ -3227,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; } @@ -3272,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); + } } } 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/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 302b5b959..4fd2c36a1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -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, diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 25a9ba4aa..8e904d0d8 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 7820a24dc..f0eae6be8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -351,21 +351,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 378052dc3..bbe7863ec 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 @@ -355,7 +361,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"; } @@ -681,19 +687,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; @@ -1502,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; } @@ -2316,7 +2325,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 3093150d4..919db14e8 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 @@ -541,7 +541,7 @@ 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) @@ -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 = @@ -591,14 +591,14 @@ namespace Barotrauma 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()) { @@ -647,7 +647,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); @@ -982,7 +982,7 @@ 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); @@ -1161,7 +1161,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 +1199,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) @@ -1279,7 +1279,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 +1299,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 +1327,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 +1345,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 +1359,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 @@ -1567,20 +1567,20 @@ namespace Barotrauma 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(); @@ -2045,7 +2045,7 @@ 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index e277590bd..3957e400a 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 765b8de83..08d7ea70c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -244,7 +244,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/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 26f10fd92..e299a8edb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -63,15 +63,16 @@ namespace Barotrauma } 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 @@ -137,12 +138,14 @@ namespace Barotrauma private float retryTimer; protected override void Act(float deltaTime) { + if (resetPriority) { return; } var currentHull = character.CurrentHull; + bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; - if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull)) + 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)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 3e5531d20..9604579dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -178,7 +178,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 +197,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..b3cdaed85 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; } @@ -391,10 +391,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 +513,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 +561,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/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/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/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..20be4494f 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 == "paralysis") + { + vitality -= affliction.Strength; + } + else if (affliction.Prefab.AfflictionType == "poison") + { + 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/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 0b1d4cafd..bb4f57c72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -454,7 +454,7 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = false; + IsHanging = IsHanging && character.IsRagdolled; } void UpdateStanding() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5dc7678ae..5ee296c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -489,7 +489,7 @@ namespace Barotrauma LocalizedString displayName = Params.DisplayName; if (displayName.IsNullOrWhiteSpace()) { - if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) + if (Params.SpeciesTranslationOverride.IsEmpty) { displayName = TextManager.Get($"Character.{SpeciesName}"); } @@ -752,7 +752,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.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength); } } @@ -822,6 +822,7 @@ namespace Barotrauma 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 { @@ -1040,6 +1041,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; @@ -2871,6 +2874,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) @@ -3791,9 +3811,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) @@ -3803,6 +3824,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 @@ -4122,13 +4148,13 @@ 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); @@ -4143,26 +4169,84 @@ 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 == "poison" || affliction.Prefab.AfflictionType == "paralysis") { - 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)); } /// @@ -5240,7 +5324,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/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 81b6598ac..997d13a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -321,6 +321,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) { @@ -335,8 +336,10 @@ 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); } } } @@ -415,8 +418,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; @@ -427,6 +430,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(); @@ -528,6 +545,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()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 4aeeb1ecb..dcfea21dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -708,7 +708,7 @@ namespace Barotrauma return; } } - if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } + if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; } if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 9771ec56e..79d2eadc6 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,14 @@ 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 == "emp") { finalDamageModifier *= character.EmpVulnerability; } + if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")) + { + 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 44018922f..04204dc96 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; } @@ -501,6 +501,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index bf290e9ae..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)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 509ed5547..a6cb260a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1868,6 +1868,7 @@ 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)); InitProjectSpecific(); 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/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/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 06d74e3fc..ddfcd55e0 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; @@ -1114,7 +1113,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 5933cf203..d902e245a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -546,9 +546,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 @@ -1074,7 +1072,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/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..f627284a2 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; @@ -308,6 +309,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 bbf59eacf..3ef615e8a 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 cf33a8eff..96d89ddc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -1,12 +1,9 @@ using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; #if CLIENT using Barotrauma.Lights; #endif @@ -206,6 +203,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()) { @@ -359,7 +358,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 c2b5630bc..b59784eeb 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).")] @@ -552,6 +562,7 @@ namespace Barotrauma.Items.Components { return false; } + bool wasAttached = IsAttached; if (base.OnPicked(picker)) { DeattachFromWall(); @@ -560,7 +571,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); } @@ -688,16 +699,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; } } @@ -875,9 +892,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..548147bfc 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(); @@ -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/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index cd81939f5..8b8ff6601 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -205,12 +205,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 1643336eb..9d8b2e08e 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; } /// @@ -697,7 +694,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..68c59aaee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -114,7 +114,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 +142,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool AllowAccess { get; set; } + [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } @@ -534,12 +537,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 +578,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 08897d67a..23c9487a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -112,27 +112,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 { @@ -457,36 +464,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); } } @@ -993,8 +1000,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 @@ -1003,8 +1010,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)); } } } @@ -1012,13 +1019,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; } @@ -1028,7 +1034,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/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/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 6d45bad3c..da887ef93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -187,6 +187,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, 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) @@ -241,6 +248,7 @@ namespace Barotrauma.Items.Components SetLightSourceState(IsActive); turret = item.GetComponent(); #if CLIENT + Drawable = AlphaBlend && Light.LightSprite != null; if (Screen.Selected.IsEditor) { OnMapLoaded(); 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/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 67084c123..bd06a6ed3 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"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 447b99409..c31f6c337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -585,6 +585,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 628a81168..346f772dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -897,7 +897,7 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition = Prefab.Health; + condition = MaxCondition = prevCondition = Prefab.Health; ConditionPercentage = 100.0f; lastSentCondition = condition; @@ -999,13 +999,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; } } @@ -1020,6 +1013,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) { @@ -1751,6 +1752,7 @@ namespace Barotrauma RecalculateConditionValues(); + bool wasPreviousConditionChanged = false; if (condition == 0.0f && prevCondition > 0.0f) { //Flag connections to be updated as device is broken @@ -1763,6 +1765,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) @@ -1793,9 +1797,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) { @@ -2696,7 +2709,7 @@ namespace Barotrauma return; } - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2713,7 +2726,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; } } @@ -2727,7 +2740,7 @@ namespace Barotrauma public void SecondaryUse(float deltaTime, Character character = null) { - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2763,6 +2776,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)); @@ -2783,13 +2803,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 db4471080..761e84d24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -769,6 +769,9 @@ 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; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -938,14 +941,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; 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/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/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 1ac0bfd73..f926af7ca 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,10 +143,16 @@ namespace Barotrauma Biome = Biome.Prefabs.First(); } - string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); + 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))); - string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); + string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 19224427b..7fa9b6fc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -559,12 +559,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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index c40ed58c8..a5c3ffac7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -531,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 ?? ""; @@ -553,7 +557,7 @@ namespace Barotrauma { realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; } - return realWorldCrushDepth; + return structureCrushDepthsDefined; } //saving/loading ---------------------------------------------------- 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/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index e3f946325..29855637a 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> @@ -512,6 +513,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/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/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 8108d7fe8..9e662479a 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); } } @@ -566,13 +575,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index c891bb02d..9f3d59506 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1455,7 +1455,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; } } @@ -1467,7 +1467,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); } } @@ -1498,17 +1498,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 + } + } } } @@ -2073,14 +2074,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); } } @@ -2111,17 +2112,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 + } + } } } } @@ -2170,7 +2172,7 @@ namespace Barotrauma { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } @@ -2183,23 +2185,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..78cd9451d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -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/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 6edb6b94f..f7b33b9aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -546,7 +546,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/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 872ba4426..8b736ad74 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; 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/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs new file mode 100644 index 000000000..9f65e0ef8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -0,0 +1,266 @@ +#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( + DateTime.SpecifyKind( + value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + DateTimeKind.Local)); + + 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 cd2fbd47e..8f63361d2 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,19 +1,60 @@ --------------------------------------------------------------------------------------------------------- -v0.21.1.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: -- Added translations for the submarine and character editors. - 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. -- Reduceed Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. +- 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. @@ -31,11 +72,17 @@ Talents: - 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 too. - - Automatic restart works in the campaign mode too. + - 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. @@ -46,9 +93,16 @@ Multiplayer: - 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. @@ -61,15 +115,40 @@ Bugfixes: - 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 in water. +- 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 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. @@ -80,6 +159,10 @@ Modding: - 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/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs b/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs new file mode 100644 index 000000000..e54c33018 --- /dev/null +++ b/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs @@ -0,0 +1,55 @@ +using Barotrauma.Utils; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using System; +using Xunit; +namespace TestProject; + +public class CoordinateSpace2DTests +{ + class CustomGenerators + { + public static Arbitrary Vector2Generator() + { + const int intRange = 1 << 22; + const float intToFloat = 1 << 19; + + return Arb.From( + from int x in Gen.Choose(-intRange, intRange) + from int y in Gen.Choose(-intRange, intRange) + select new Vector2(x / intToFloat, y / intToFloat)); + } + } + + public CoordinateSpace2DTests() + { + Arb.Register(); + } + + [Fact] + public void TestLocalToCanonical() + { + void testCase(Tuple args) + { + var (vector, origin, i, j) = args; + + if (Vector2.DistanceSquared(i, j) <= 0.01f) { return; } + + var space = new CoordinateSpace2D + { + Origin = origin, + I = i, + J = j + }; + + Assert.True(Vector2.DistanceSquared( + Vector2.Transform(vector, space.LocalToCanonical), + origin + vector.X * i + vector.Y * j) < 0.01f); + } + + Prop.ForAll( + Arb.Generate>().ToArbitrary(), + testCase).QuickCheckThrowOnFailure(); + } +} diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs index 86213b54d..76d2a689c 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -10,7 +10,7 @@ using Xunit; namespace TestProject; -public class INetSerializableStructImplementationChecks +public sealed class INetSerializableStructImplementationChecks { private delegate bool TryFindBehaviorDelegate(Type type, out NetSerializableProperties.IReadWriteBehavior behavior); @@ -49,7 +49,7 @@ public class INetSerializableStructImplementationChecks viableArguments.AddRange(new[] { typeof(Vector2), - typeof(Point), + typeof(float), typeof(int) }); } diff --git a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs new file mode 100644 index 000000000..12cdb9243 --- /dev/null +++ b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using Barotrauma; +using FluentAssertions; +using FsCheck; +using Xunit; + +namespace TestProject; + +public sealed class SerializableDateTimeTests +{ + private class CustomGenerators + { + private const short MinutesPerDay = 24 * 60; + private const int SecondsPerDay = MinutesPerDay * 60; + + public static Arbitrary SerializableDateTimeGenerator() + { + return Arb.From( + from int dateTimeDay in Gen.Choose(0, (int)(DateTime.MaxValue.Ticks / TimeSpan.TicksPerDay)) + from int dateTimeSeconds in Gen.Choose(0, SecondsPerDay) + from int timeZoneMinutes in Gen.Choose(-MinutesPerDay / 2, MinutesPerDay / 2) + select new SerializableDateTime( + DateTime.MinValue + TimeSpan.FromDays(dateTimeDay) + TimeSpan.FromSeconds(dateTimeSeconds), + new SerializableTimeZone(TimeSpan.FromMinutes(timeZoneMinutes)))); + } + } + + public SerializableDateTimeTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void EqualityTest() + { + Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); + } + + [Fact] + public void ParseTest() + { + var parseTest = "9369Y 09M 06D 03HR 43MIN 09SEC UTC+8:49"; + SerializableDateTime.Parse(parseTest); + Prop.ForAll(ParseCheck).QuickCheckThrowOnFailure(); + } + + private static void EqualityCheck(SerializableDateTime original) + { + var local = original.ToLocal(); + var utc = original.ToUtc(); + original.Should().BeEquivalentTo(local); + original.Should().BeEquivalentTo(utc); + local.Should().BeEquivalentTo(utc); + } + + private static void ParseCheck(SerializableDateTime original) + { + var str = original.ToString(); + SerializableDateTime.Parse(str).TryUnwrap(out var parsedTime).Should().BeTrue(); + parsedTime.Should().BeEquivalentTo(original); + } +} diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs index 6b36c44e1..479273e9e 100644 --- a/Barotrauma/BarotraumaTest/TestProject.cs +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -10,8 +10,8 @@ namespace TestProject { public static Arbitrary Vector2Generator() { - return Arb.From(from int x in Arb.Generate() - from int y in Arb.Generate() + return Arb.From(from float x in Arb.Generate().Where(f => !float.IsNaN(f) && !float.IsInfinity(f)) + from float y in Arb.Generate().Where(f => !float.IsNaN(f) && !float.IsInfinity(f)) select new Vector2(x, y)); } 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; } };