From cf9ecd35b39c749935d773b99ac268ea1dee9d52 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 31 Jan 2023 18:08:26 +0200 Subject: [PATCH] Build 0.21.6.0 (1.0 pre-patch) --- .editorconfig | 6 + .gitattributes | 3 +- .../ClientSource/CameraTransition.cs | 14 +- .../Characters/Animation/Ragdoll.cs | 21 +- .../ClientSource/Characters/Character.cs | 4 +- .../ClientSource/Characters/CharacterHUD.cs | 5 +- .../ClientSource/Characters/CharacterInfo.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 7 +- .../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 | 7 +- .../ClientSource/GUI/TalentMenu.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 256 ++++++++++++----- .../BarotraumaClient/ClientSource/GameMain.cs | 61 ++-- .../ClientSource/GameSession/CrewManager.cs | 10 +- .../GameSession/GameModes/CampaignMode.cs | 7 +- .../ClientSource/Items/CharacterInventory.cs | 18 +- .../ClientSource/Items/Components/Door.cs | 58 ++-- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Items/Components/ItemContainer.cs | 79 ++++++ .../Items/Components/LightComponent.cs | 26 +- .../Items/Components/Machines/MiniMap.cs | 7 +- .../Items/Components/Machines/Reactor.cs | 13 +- .../Components/Signal/CustomInterface.cs | 1 + .../Items/Components/Signal/Wire.cs | 57 ++-- .../ClientSource/Items/Components/Wearable.cs | 5 +- .../ClientSource/Items/Inventory.cs | 93 +----- .../ClientSource/Items/Item.cs | 10 +- .../ClientSource/Items/ItemPrefab.cs | 16 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 4 +- .../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/MapEntity.cs | 4 +- .../ClientSource/Map/RoundSound.cs | 5 +- .../ClientSource/Map/SubmarineInfo.cs | 74 ++++- .../ClientSource/Map/SubmarinePreview.cs | 138 ++++++--- .../ClientSource/Networking/BanList.cs | 30 +- .../ClientSource/Networking/ChatMessage.cs | 7 +- .../Networking/ChildServerRelay.cs | 1 + .../ClientSource/Networking/GameClient.cs | 101 ++++--- .../ClientEntityEventManager.cs | 19 +- .../Networking/Primitives/Peers/ClientPeer.cs | 7 + .../Networking/Voip/VoipClient.cs | 2 +- .../ClientSource/Networking/Voting.cs | 7 +- .../CampaignSetupUI/CampaignSetupUI.cs | 50 +++- .../MultiPlayerCampaignSetupUI.cs | 58 +--- .../SinglePlayerCampaignSetupUI.cs | 112 +++----- .../ClientSource/Screens/CampaignUI.cs | 1 + .../CharacterEditor/CharacterEditorScreen.cs | 39 +-- .../Screens/EventEditor/EventEditorScreen.cs | 9 + .../ClientSource/Screens/GameScreen.cs | 22 +- .../ClientSource/Screens/MainMenuScreen.cs | 35 ++- .../ClientSource/Screens/NetLobbyScreen.cs | 16 +- .../ClientSource/Screens/SubEditorScreen.cs | 21 +- .../ClientSource/Screens/TestScreen.cs | 3 +- .../Serialization/SerializableEntityEditor.cs | 14 +- .../ClientSource/Sounds/SoundManager.cs | 6 + .../ClientSource/Sounds/SoundPlayer.cs | 13 +- .../ClientSource/Sprite/DeformableSprite.cs | 7 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 183 ++++++------ .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 14 + .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 7 +- .../ClientSource/Upgrades/UpgradePrefab.cs | 2 +- .../ClientSource/Utils/EffectLoader.cs | 12 + .../Utils/LocalizationCSVtoXML.cs | 86 ++++-- .../ClientSource/Utils/SpriteRecorder.cs | 198 +++++++------ .../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 + .../Characters/CharacterNetworking.cs | 45 ++- .../Health/HealingCooldownServer.cs | 51 ++++ .../ServerSource/DebugConsole.cs | 2 +- .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameSession/GameModes/CampaignMode.cs | 7 +- .../GameModes/CharacterCampaignData.cs | 11 +- .../GameModes/MultiPlayerCampaign.cs | 20 +- .../ServerSource/Items/Inventory.cs | 19 +- .../ServerSource/Items/Item.cs | 26 +- .../ServerSource/Map/Submarine.cs | 7 +- .../ServerSource/Networking/BanList.cs | 53 ++-- .../ServerSource/Networking/GameServer.cs | 166 ++++++----- .../Networking/OrderChatMessage.cs | 1 + .../Primitives/Peers/Server/ServerPeer.cs | 2 +- .../ServerSource/Networking/RespawnManager.cs | 3 +- .../ServerSource/Networking/Voting.cs | 19 +- .../ServerSource/Screens/NetLobbyScreen.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 5 +- .../Characters/AI/EnemyAIController.cs | 44 +-- .../Characters/AI/HumanAIController.cs | 115 ++++---- .../AI/Objectives/AIObjectiveCombat.cs | 16 +- .../AI/Objectives/AIObjectiveContainItem.cs | 5 +- .../Objectives/AIObjectiveFindDivingGear.cs | 45 ++- .../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 | 20 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 25 +- .../Characters/AI/Wreck/WreckAI.cs | 6 +- .../Characters/Animation/AnimController.cs | 8 +- .../Animation/FishAnimController.cs | 4 +- .../Animation/HumanoidAnimController.cs | 78 +++-- .../Characters/Animation/Ragdoll.cs | 43 ++- .../SharedSource/Characters/Attack.cs | 90 ++++-- .../SharedSource/Characters/Character.cs | 263 ++++++++++++----- .../SharedSource/Characters/CharacterInfo.cs | 7 +- .../Health/Afflictions/AfflictionPrefab.cs | 41 ++- .../Characters/Health/CharacterHealth.cs | 22 +- .../SharedSource/Characters/Limb.cs | 103 ++++--- .../Characters/Params/CharacterParams.cs | 5 +- .../AbilityConditionAttackData.cs | 7 +- .../AbilityConditionCharacter.cs | 1 - .../AbilityConditionItem.cs | 31 +- .../CharacterAbilityGiveItemStatToTags.cs | 8 + .../CharacterAbilityRemoveRandomIngredient.cs | 22 +- .../ContentPackage/ContentPackage.cs | 13 +- .../ContentPackageManager.cs | 15 +- .../ContentManagement/ContentXElement.cs | 1 + .../SharedSource/DebugConsole.cs | 9 +- .../BarotraumaShared/SharedSource/Enums.cs | 1 - .../Events/EventActions/UIHighlightAction.cs | 4 +- .../Missions/AbandonedOutpostMission.cs | 2 +- .../Events/Missions/BeaconMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 9 +- .../SharedSource/Events/MonsterEvent.cs | 2 +- .../GameAnalytics/GameAnalyticsManager.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 14 +- .../GameSession/GameModes/CampaignSettings.cs | 2 +- .../GameSession/GameModes/GameModePreset.cs | 16 +- .../SharedSource/GameSession/GameSession.cs | 9 +- .../GameSession/UpgradeManager.cs | 43 ++- .../SharedSource/Items/CharacterInventory.cs | 5 +- .../Items/Components/DockingPort.cs | 31 +- .../SharedSource/Items/Components/Door.cs | 16 +- .../Items/Components/Holdable/Holdable.cs | 39 ++- .../Items/Components/Holdable/MeleeWeapon.cs | 113 +++++--- .../Items/Components/Holdable/Pickable.cs | 2 + .../Items/Components/Holdable/RangedWeapon.cs | 2 + .../Items/Components/Holdable/RepairTool.cs | 31 +- .../Items/Components/Holdable/Throwable.cs | 4 +- .../Items/Components/ItemComponent.cs | 19 +- .../Items/Components/ItemContainer.cs | 11 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Projectile.cs | 70 +++-- .../SharedSource/Items/Components/Quality.cs | 10 +- .../SharedSource/Items/Components/Rope.cs | 46 ++- .../Items/Components/Signal/LightComponent.cs | 39 ++- .../Items/Components/Signal/WifiComponent.cs | 14 +- .../Items/Components/Signal/Wire.cs | 23 +- .../SharedSource/Items/Components/Turret.cs | 4 +- .../SharedSource/Items/Components/Wearable.cs | 24 +- .../SharedSource/Items/Inventory.cs | 9 +- .../SharedSource/Items/Item.cs | 87 +++--- .../SharedSource/Items/ItemEventData.cs | 2 +- .../SharedSource/Items/ItemPrefab.cs | 25 +- .../SharedSource/Map/Entity.cs | 26 +- .../SharedSource/Map/Explosion.cs | 6 + .../SharedSource/Map/ItemAssemblyPrefab.cs | 6 + .../SharedSource/Map/Levels/Level.cs | 24 +- .../SharedSource/Map/Levels/LevelData.cs | 18 +- .../SharedSource/Map/Map/Location.cs | 7 +- .../SharedSource/Map/Map/Map.cs | 4 +- .../SharedSource/Map/MapEntityPrefab.cs | 3 + .../Map/Outposts/OutpostGenerator.cs | 13 +- .../SharedSource/Map/StructurePrefab.cs | 4 +- .../SharedSource/Map/Submarine.cs | 92 +++--- .../SharedSource/Map/SubmarineBody.cs | 6 +- .../SharedSource/Map/SubmarineInfo.cs | 12 +- .../SharedSource/Networking/BanList.cs | 2 +- .../SharedSource/Networking/Client.cs | 2 +- .../Networking/INetSerializable.cs | 2 +- .../Networking/INetSerializableStruct.cs | 38 ++- .../SharedSource/Networking/NetworkMember.cs | 13 + .../Primitives/NetworkPeerStructs.cs | 4 +- .../SharedSource/Networking/ServerSettings.cs | 2 +- .../SharedSource/Networking/Voting.cs | 4 +- .../Serialization/XMLExtensions.cs | 84 +++++- .../SharedSource/Settings/GameSettings.cs | 2 - .../StatusEffects/PropertyConditional.cs | 1 + .../StatusEffects/StatusEffect.cs | 75 ++--- .../SharedSource/Steam/Workshop.cs | 5 +- .../SharedSource/SteamAchievementManager.cs | 2 + .../SharedSource/Upgrades/Upgrade.cs | 2 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 113 +++++++- .../SharedSource/Utils/CoordinateSpace2D.cs | 26 ++ .../SharedSource/Utils/MathUtils.cs | 3 + .../SharedSource/Utils/Md5Hash.cs | 7 +- .../SharedSource/Utils/Range.cs | 2 +- .../SharedSource/Utils/SaveUtil.cs | 22 +- .../SharedSource/Utils/SegmentTable.cs | 20 +- .../Utils/SerializableDateTime.cs | 266 ++++++++++++++++++ .../SharedSource/Utils/ToolBox.cs | 43 --- Barotrauma/BarotraumaShared/changelog.txt | 166 +++++++++++ .../BarotraumaTest/CoordinateSpace2DTests.cs | 55 ++++ ...tSerializableStructImplementationChecks.cs | 105 ++++--- .../INetSerializableStructTests.cs | 32 ++- .../SerializableDateTimeTests.cs | 64 +++++ Barotrauma/BarotraumaTest/TestProject.cs | 4 +- .../Classes/AuthTicket.cs | 31 +- .../SteamMatchmakingResponses.cs | 143 ++++++---- .../Utility/SourceServerQuery.cs | 235 ++++------------ Libraries/Lidgren.Network/NetPeer.Internal.cs | 27 +- .../NetPeer.LatencySimulation.cs | 2 +- Libraries/Lidgren.Network/NetPeer.cs | 2 +- Libraries/Lidgren.Network/NetUtility.cs | 30 +- .../Graphics/SpriteBatch.cs | 25 +- 231 files changed, 4479 insertions(+), 2276 deletions(-) create mode 100644 .editorconfig 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/.editorconfig b/.editorconfig new file mode 100644 index 000000000..4ee70dd42 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*.cs] + +# IDE0090: Use 'new(...)' +csharp_style_implicit_object_creation_when_type_is_apparent = false + +dotnet_diagnostic.CA1806.severity = silent diff --git a/.gitattributes b/.gitattributes index 56f8e94c2..219869ee0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Declare files that will always have CRLF line endings on checkout. *.sln text eol=crlf *.cs text eol=crlf -*.xml text eol=crlf \ No newline at end of file +*.xml text eol=crlf +Barotrauma\BarotraumaServer\DedicatedServer.exe text eol=lf \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index 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/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 007c6d8a3..a9fa83ac4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -107,7 +107,7 @@ namespace Barotrauma Collider.AngularVelocity = newAngularVelocity; float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove ? 0.01f : 0.2f; + float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { if (distSqrd > 10.0f || !character.CanMove) @@ -145,6 +145,7 @@ namespace Barotrauma { MainLimb.PullJointWorldAnchorB = Collider.SimPosition; MainLimb.PullJointEnabled = true; + MainLimb.body.LinearVelocity = newVelocity; } } } @@ -442,10 +443,20 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; } - Vector2 spriteOrigin = limb.ActiveSprite.Origin; - spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; - limb.ActiveSprite.Origin = spriteOrigin; + if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } + + FlipSprite(limb.DeformSprite?.Sprite ?? limb.Sprite); + foreach (var conditionalSprite in limb.ConditionalSprites) + { + FlipSprite(conditionalSprite.DeformableSprite?.Sprite ?? conditionalSprite.Sprite); + } + } + static void FlipSprite(Sprite sprite) + { + if (sprite == null) { return; } + Vector2 spriteOrigin = sprite.Origin; + spriteOrigin.X = sprite.SourceRect.Width - spriteOrigin.X; + sprite.Origin = spriteOrigin; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 770aa20e8..d20050fe4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -616,7 +616,7 @@ namespace Barotrauma return closestItem; } - private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) + private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = MaxHighlightDistance) { Character closestCharacter = null; @@ -626,7 +626,7 @@ namespace Barotrauma { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } - float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); + float dist = c.GetDistanceToClosestLimb(mouseSimPos); if (dist < closestDist || (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index ff196c018..80b153e28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -608,7 +608,8 @@ namespace Barotrauma } } - Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; + float dist = Vector2.Distance(character.FocusedCharacter.DrawPosition, character.DrawPosition); + Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) / dist * Math.Min(dist, Character.MaxDragDistance); startPos = cam.WorldToScreen(startPos); string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; @@ -723,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/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index b472749ff..c4bdcaba9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -938,7 +938,7 @@ namespace Barotrauma var headPreset = obj as HeadPreset; if (info.Head.Preset != headPreset) { - info.Head = new HeadInfo(info, headPreset) + info.Head = new HeadInfo(info, headPreset, info.Head.HairIndex, info.Head.BeardIndex, info.Head.MoustacheIndex, info.Head.FaceAttachmentIndex) { SkinColor = info.Head.SkinColor, HairColor = info.Head.HairColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index f39de7890..6fe86291d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -640,8 +640,7 @@ namespace Barotrauma else { forceAfflictionContainerUpdate = true; - currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true) - .FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); + currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); @@ -1275,7 +1274,7 @@ namespace Barotrauma //displaying an affliction we no longer have -> dirty foreach ((Affliction affliction, float strength) in displayedAfflictions) { - if (!afflictions.Any(a => a.Key == affliction)) { return true; } + if (afflictions.None(a => a.Key == affliction && a.Key.ShouldShowIcon(Character))) { return true; } } return false; } @@ -2072,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 781483117..75219550f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1766,7 +1766,7 @@ namespace Barotrauma { foreach (UpgradePrefab prefab in categoryData.Prefabs) { - var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false); + var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false).Frame; UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign); } } @@ -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/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 354ef4180..9b5318e30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -796,7 +796,7 @@ namespace Barotrauma CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; if (ownCharacterInfo is null) { return false; } - return info == ownCharacterInfo; + return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName(); } public static bool CanManageTalents(CharacterInfo targetInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 445a77787..3a2e531c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -90,6 +89,16 @@ namespace Barotrauma Repairs } + private enum UpgradeStoreUserData + { + BuyButton, + BuyButtonLayout, + ProgressBarLayout, + IncreaseLabel, + PriceLabel, + MaterialCostList + } + public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; @@ -600,7 +609,7 @@ namespace Barotrauma GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); - GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; + GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = UpgradeStoreUserData.BuyButtonLayout }; new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); @@ -950,7 +959,7 @@ namespace Barotrauma frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, - 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); + 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton) { @@ -987,11 +996,11 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, - price, replacement, - addBuyButton: true, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + price, replacement, + addBuyButton: true, addProgressBar: false, - buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); + buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton").Frame); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } if (PlayerBalance >= price) @@ -1081,13 +1090,23 @@ namespace Barotrauma }; } - public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) + public readonly record struct BuyButtonFrame(GUILayoutGroup Layout, GUIListBox MaterialCostList, GUIButton BuyButton, GUITextBlock PriceText); + public readonly record struct ProgressBarFrame(GUITextBlock ProgressText, GUIProgressBar ProgressBar); + + public readonly record struct UpgradeFrame(GUIFrame Frame, + GUIImage Icon, + GUITextBlock Name, + GUITextBlock Description, + Option BuyButton, + Option ProgressBar); + + public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } - public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) + public static UpgradeFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) { float progressBarHeight = 0.25f; @@ -1105,21 +1124,26 @@ namespace Barotrauma * |------------------------------------------------------------------| */ GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData }; - GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: true) { Stretch = true }; - GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); - var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; - GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; - GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); - var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; - GUILayoutGroup? progressLayout = null; + GUILayoutGroup mainLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: false); + GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(1f, addBuyButton ? 0.65f : 1f, mainLayout, Anchor.Center), isHorizontal: true) { Stretch = true }; + GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); + var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; + GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); + var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + GUILayoutGroup? progressLayout = null; GUILayoutGroup? buyButtonLayout = null; + Option buyButtonOption = Option.None(); + Option progressBarOption = Option.None(); + if (addProgressBar) { - progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; - new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); - new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = UpgradeStoreUserData.ProgressBarLayout }; + GUITextBlock progressText = new GUITextBlock(rectT(0.15f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + GUIProgressBar progressBar = new GUIProgressBar(rectT(0.85f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); + progressBarOption = Option.Some(new ProgressBarFrame(progressText, progressBar)); } if (addBuyButton) @@ -1127,12 +1151,33 @@ namespace Barotrauma var formattedPrice = TextManager.FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } - buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center) + buyButtonLayout = new GUILayoutGroup(rectT(1f, 0.35f, mainLayout), isHorizontal: true) { UserData = UpgradeStoreUserData.BuyButtonLayout };; + + GUIListBox materialCostList; + if (upgradePrefab is not null) { + var increaseText = new GUITextBlock(rectT(imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), "", textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) + { + UserData = UpgradeStoreUserData.IncreaseLabel + }; + UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel); + materialCostList = new GUIListBox(rectT(0.65f - imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), isHorizontal: true, style: null); + } + else + { + materialCostList = new GUIListBox(rectT(0.65f, 1f, buyButtonLayout), isHorizontal: true, style: null); + } + + materialCostList.Visible = false; + materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; + + 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 Visible = userData is ItemPrefab }; + if (price < 0) { priceText.TextColor = GUIStyle.Green; @@ -1141,15 +1186,13 @@ namespace Barotrauma { priceText.Text = string.Empty; } - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) + GUIButton buyButton = new GUIButton(rectT(0.15f, 1f, buyButtonLayout), string.Empty, style: buttonStyle) { + UserData = UpgradeStoreUserData.BuyButton, Enabled = false }; - if (upgradePrefab != null) - { - var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); - UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel); - } + + buyButtonOption = Option.Some(new BuyButtonFrame(buyButtonLayout, materialCostList, buyButton, priceText)); } description.CalculateHeightFromText(); @@ -1175,7 +1218,7 @@ namespace Barotrauma progressLayout?.Recalculate(); buyButtonLayout?.Recalculate(); - return prefabFrame; + return new UpgradeFrame(prefabFrame, icon, name, description, buyButtonOption, progressBarOption); } private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel) @@ -1197,31 +1240,21 @@ namespace Barotrauma Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; if (Campaign is null || sub is null) { return; } - GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); - var prefabLayout = prefabFrame.GetChild(); - GUILayoutGroup[] childLayouts = prefabLayout.GetAllChildren().ToArray(); - var imageLayout = childLayouts[0]; - var icon = imageLayout.GetChild(); - var textLayout = childLayouts[1]; - var name = textLayout.GetChild(); - GUILayoutGroup[] textChildLayouts = textLayout.GetAllChildren().ToArray(); - var descriptionLayout = textChildLayouts[0]; - var description = descriptionLayout.GetChild(); - var progressLayout = textChildLayouts[1]; - var buyButtonLayout = childLayouts[2]; - var buyButton = buyButtonLayout.GetChild(); + UpgradeFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.4f, parent)); + + if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; } if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) { - prefabFrame.Enabled = false; - description.Enabled = false; - name.Enabled = false; - icon.Color = Color.Gray; - buyButton.Enabled = false; - buyButtonLayout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button + prefabFrame.Frame.Enabled = false; + prefabFrame.Description.Enabled = false; + prefabFrame.Name.Enabled = false; + prefabFrame.Icon.Color = Color.Gray; + buyButtonFrame.BuyButton.Enabled = false; + buyButtonFrame.Layout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button } - buyButton.OnClicked += (button, o) => + buyButtonFrame.BuyButton.OnClicked += (button, o) => { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), @@ -1240,7 +1273,7 @@ namespace Barotrauma return true; }; - UpdateUpgradeEntry(prefabFrame, prefab, category, Campaign); + UpdateUpgradeEntry(prefabFrame.Frame, prefab, category, Campaign); } private void CreateItemTooltip(MapEntity entity) @@ -1623,7 +1656,7 @@ namespace Barotrauma int maxLevel = prefab.GetMaxLevelForCurrentSub(); LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString())); - if (prefabFrame.FindChild("progressbar", true) is { } progressParent) + if (prefabFrame.FindChild(UpgradeStoreUserData.ProgressBarLayout, true) is { } progressParent) { GUIProgressBar bar = progressParent.GetChild(); if (bar != null) @@ -1636,36 +1669,111 @@ namespace Barotrauma if (block != null) { block.Text = progressText; } } - if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) + if (prefabFrame.FindChild(UpgradeStoreUserData.BuyButtonLayout, true) is not { } buttonParent) { return; } + + GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); + priceLabel.Visible = true; + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + + if (!WaitForServerUpdate) { - List textBlocks = buttonParent.GetAllChildren().ToList(); - - GUITextBlock priceLabel = textBlocks[0]; - priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); - - if (priceLabel != null && !WaitForServerUpdate) + priceLabel.Text = TextManager.FormatCurrency(price); + if (currentLevel >= maxLevel) { - priceLabel.Text = TextManager.FormatCurrency(price); - if (currentLevel >= maxLevel) - { - priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); - } + priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); } + } - GUIButton button = buttonParent.GetChild(); - if (button != null) + if (buttonParent.FindChild(UpgradeStoreUserData.IncreaseLabel, recursive: true) is GUITextBlock increaseLabel && !WaitForServerUpdate) + { + UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); + } + + bool isMax = currentLevel >= maxLevel; + + if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button) + { + bool canBuy = !WaitForServerUpdate && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1); + + button.Enabled = canBuy; + } + + if (prefabFrame.FindChild(UpgradeStoreUserData.MaterialCostList, true) is GUIListBox itemList) + { + if (isMax) { - button.Enabled = currentLevel < maxLevel; - if (WaitForServerUpdate || campaign.GetBalance() < price) - { - button.Enabled = false; - } + itemList.Visible = false; } - GUITextBlock increaseLabel = textBlocks[1]; - if (increaseLabel != null && !WaitForServerUpdate) + else { - UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); + CreateMaterialCosts(itemList, prefab, currentLevel + 1); + } + } + + static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) + { + list.Content.ClearChildren(); + List allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List(); + + var resources = prefab.GetApplicableResources(targetLevel); + + foreach (ApplicableResourceCollection collection in resources) + { + list.Visible = true; + + int length = collection.MatchingItems.Length; + + if (length is 0) { continue; } + + ItemPrefab defaultItemPrefab = collection.MatchingItems.First(); + + GUILayoutGroup wrapperLayout = new GUILayoutGroup(rectT(0.25f, 1f, list.Content)); + + GUIFrame itemFrame = new GUIFrame(rectT(1f, 1f, wrapperLayout), style: null) + { + ToolTip = defaultItemPrefab.Name + }; + + bool hasItems = collection.Cost.Amount <= allItems.Count(collection.Cost.MatchesItem); + + Sprite icon = defaultItemPrefab.InventoryIcon ?? prefab.Sprite; + Color iconColor = defaultItemPrefab.InventoryIcon is null ? defaultItemPrefab.SpriteColor : defaultItemPrefab.InventoryIconColor; + + GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true) + { + Color = hasItems ? iconColor : iconColor * 0.9f, + CanBeFocused = false + }; + + // item count text + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), itemIcon.RectTransform, anchor: Anchor.BottomRight), $"{collection.Count}", font: GUIStyle.Font, textAlignment: Alignment.BottomRight) + { + Shadow = true, + CanBeFocused = false, + Padding = Vector4.Zero, + TextColor = hasItems ? Color.White : GUIStyle.Red, + }; + + if (length is 1) { continue; } + + // we have more than 1 item, show a "slideshow" of the items + + float index = 0f; + GUICustomComponent customComponent = new GUICustomComponent(rectT(1f, 1f, itemFrame), null, (deltaTime, component) => + { + index += deltaTime / 3f; + if (index > length) { index = 0; } + + ItemPrefab currentPrefab = collection.MatchingItems[(int)MathF.Floor(index)]; + Sprite icon = currentPrefab.InventoryIcon ?? prefab.Sprite; + Color iconColor = currentPrefab.InventoryIcon is null ? currentPrefab.SpriteColor : currentPrefab.InventoryIconColor; + itemIcon.Sprite = icon; + itemIcon.Color = hasItems ? iconColor : iconColor * 0.9f; + itemFrame.ToolTip = currentPrefab.Name; + }) + { + CanBeFocused = false + }; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 8bc5fa434..a730310d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -17,14 +17,19 @@ using System.Linq; using System.Reflection; using System.Threading; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { class GameMain : Game { - public static bool ShowFPS = false; - public static bool ShowPerf = false; + public static bool ShowFPS; + public static bool ShowPerf; public static bool DebugDraw; + /// + /// Doesn't automatically enable los or bot AI or do anything like that. Probably not fully implemented. + /// + public static bool DevMode; public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; @@ -397,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); @@ -475,6 +480,19 @@ namespace Barotrauma yield return CoroutineStatus.Running; } + var corePackage = ContentPackageManager.EnabledPackages.Core; + if (corePackage.EnableError.TryUnwrap(out var error)) + { + if (error.ErrorsOrException.TryGet(out ImmutableArray errorMessages)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {errorMessages.First()}"); + } + else if (error.ErrorsOrException.TryGet(out Exception exception)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {exception.Message}", exception); + } + } + TextManager.VerifyLanguageAvailable(); DebugConsole.Init(); @@ -498,10 +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; @@ -735,8 +753,8 @@ namespace Barotrauma { Client.Quit(); Client = null; - MainMenuScreen.Select(); } + MainMenuScreen.Select(); if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) { @@ -1099,37 +1117,6 @@ namespace Barotrauma GameSession = null; } - public void ShowEditorDisclaimer() - { - var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText")); - var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; - linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List<(LocalizedString Caption, string Url)> links = new List<(LocalizedString, string)>() - { - (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value), - (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl").Fallback("https://discordapp.com/invite/undertow").Value), - }; - foreach (var link in links) - { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.Caption, style: "MainMenuGUIButton", textAlignment: Alignment.Left) - { - UserData = link.Url, - OnClicked = (btn, userdata) => - { - ShowOpenUrlInWebBrowserPrompt(userdata as string); - return true; - } - }; - } - - msgBox.InnerFrame.RectTransform.MinSize = new Point(0, - msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + 10); - var config = GameSettings.CurrentConfig; - config.EditorDisclaimerShown = true; - GameSettings.SetCurrentConfig(config); - GameSettings.SaveCurrentConfig(); - } - public void ShowBugReporter() { if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string == "bugreporter") 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/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 856417e04..3aae45f01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -106,12 +106,7 @@ namespace Barotrauma public static bool AllowedToManageWallets() { - if (GameMain.Client == null) { return true; } - - return - GameMain.Client.HasPermission(ClientPermissions.ManageMoney) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.IsServerOwner; + return AllowedToManageCampaign(ClientPermissions.ManageMoney); } public override void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 05a85c692..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.ContainedItems.FirstOrDefault()?.Condition ?? 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/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index fd10eb0f0..f50239f35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components public void DrawElectricity(SpriteBatch spriteBatch) { - if (timer <= 0.0f) { return; } + if (timer <= 0.0f && Screen.Selected is { IsEditor: false }) { return; } for (int i = 0; i < nodes.Count; i++) { if (nodes[i].Length <= 1.0f) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index d8c379c03..5b16c7d9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,7 +1,9 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections; using System.Linq; +using static Barotrauma.Inventory; namespace Barotrauma.Items.Components { @@ -250,6 +252,83 @@ namespace Barotrauma.Items.Components return true; } + + public float GetContainedIndicatorState() + { + if (ShowConditionInContainedStateIndicator) + { + return item.Condition / item.MaxCondition; + } + + int targetSlot = Math.Max(ContainedStateIndicatorSlot, 0); + if (targetSlot >= Inventory.Capacity) { return 0.0f; } + + var containedItems = Inventory.GetItemsAt(targetSlot); + if (containedItems == null) { return 0.0f; } + + Item containedItem = containedItems.FirstOrDefault(); + if (ShowTotalStackCapacityInContainedStateIndicator) + { + // No item on the defined slot, check if the items on other slots can be used. + containedItem ??= + containedItems.FirstOrDefault() ?? + Inventory.AllItems.FirstOrDefault(it => CanBeContained(it, targetSlot)); + if (containedItem == null) { return 0.0f; } + + int ignoredItemCount = 0; + var subContainableItems = AllSubContainableItems; + float capacity = GetMaxStackSize(targetSlot); + if (subContainableItems != null) + { + bool useMainContainerCapacity = true; + foreach (Item it in Inventory.AllItems) + { + // Ignore all items in the sub containers. + foreach (RelatedItem ri in subContainableItems) + { + if (ri.MatchesItem(containedItem)) + { + // The target item is in a subcontainer -> inverse the logic. + useMainContainerCapacity = false; + break; + } + if (ri.MatchesItem(it)) + { + ignoredItemCount++; + } + } + if (!useMainContainerCapacity) { break; } + } + if (useMainContainerCapacity) + { + capacity *= MainContainerCapacity; + } + else + { + // Ignore all items in the main container. + ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); + capacity *= Capacity - MainContainerCapacity; + } + } + int itemCount = Inventory.AllItems.Count() - ignoredItemCount; + return Math.Min(itemCount / Math.Max(capacity, 1), 1); + } + else + { + if (containedItem != null && (Inventory.Capacity == 1 || HasSubContainers)) + { + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + { + return containedItems.Count() / (float)maxStackSize; + } + } + return Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1 ? + (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : + Inventory.EmptySlotCount / (float)Inventory.Capacity; + } + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 1d4db25d2..2c764937c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; - private float currentBrightness; - public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -29,14 +27,21 @@ namespace Barotrauma.Items.Components Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } - partial void SetLightSourceState(bool enabled, float brightness) + partial void SetLightSourceState(bool enabled, float? brightness) { if (Light == null) { return; } Light.Enabled = enabled; - currentBrightness = brightness; + if (brightness.HasValue) + { + lightBrightness = brightness.Value; + } + else + { + lightBrightness = enabled ? 1.0f : 0.0f; + } if (enabled) { - Light.Color = LightColor.Multiply(brightness); + Light.Color = LightColor.Multiply(lightBrightness); } } @@ -73,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 973f6e514..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; } @@ -519,7 +519,10 @@ namespace Barotrauma.Items.Components Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None); } - }); + }) + { + CanBeFocused = false + }; weaponChilds.Add(component, frame); } 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/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index c97a2945e..6ec91dabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -25,14 +25,14 @@ namespace Barotrauma.Items.Components public static Color editorHighlightColor = Color.Yellow; public static Color editorSelectedColor = Color.Red; - partial class WireSection + public partial class WireSection { public VertexPositionColorTexture[] vertices; public VertexPositionColorTexture[] shiftedVertices; private float cachedWidth = 0f; - private void RecalculateVertices(Wire wire, float width) + private void RecalculateVertices(Sprite wireSprite, float width) { if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } cachedWidth = width; @@ -45,13 +45,13 @@ namespace Barotrauma.Items.Components expandDir.X = -expandDir.Y; expandDir.Y = -temp; - Rectangle srcRect = wire.wireSprite.SourceRect; + Rectangle srcRect = wireSprite.SourceRect; expandDir *= width * srcRect.Height * 0.5f; Vector2 rectLocation = srcRect.Location.ToVector2(); Vector2 rectSize = srcRect.Size.ToVector2(); - Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); + Vector2 textureSize = new Vector2(wireSprite.Texture.Width, wireSprite.Texture.Height); Vector2 topLeftUv = rectLocation / textureSize; Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; @@ -67,10 +67,10 @@ namespace Barotrauma.Items.Components shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); } - public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) + public void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Color color, Vector2 offset, float depth, float width = 0.3f) { if (width <= 0f) { return; } - RecalculateVertices(wire, width); + RecalculateVertices(wireSprite, width); for (int i = 0; i < vertices.Length; i++) { @@ -79,21 +79,22 @@ namespace Barotrauma.Items.Components shiftedVertices[i].Position.X += offset.X; shiftedVertices[i].Position.Y -= offset.Y; } - spriteBatch.Draw(wire.wireSprite.Texture, + spriteBatch.Draw( + wireSprite.Texture, shiftedVertices, depth); } - public static void Draw(SpriteBatch spriteBatch, Wire wire, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) + public static void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) { start.Y = -start.Y; end.Y = -end.Y; - spriteBatch.Draw(wire.wireSprite.Texture, - start, wire.wireSprite.SourceRect, color, + spriteBatch.Draw(wireSprite.Texture, + start, wireSprite.SourceRect, color, MathUtils.VectorToAngle(end - start), - new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), - new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.size.X, width), + new Vector2(0.0f, wireSprite.size.Y / 2.0f), + new Vector2((Vector2.Distance(start, end)) / wireSprite.size.X, width), SpriteEffects.None, depth); } @@ -123,7 +124,7 @@ namespace Barotrauma.Items.Components get => draggingWire; } - partial void InitProjSpecific(ContentXElement element) + public static Sprite ExtractWireSprite(ContentXElement element) { if (defaultWireSprite == null) { @@ -133,6 +134,7 @@ namespace Barotrauma.Items.Components }; } + Sprite overrideSprite = null; foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) @@ -142,9 +144,14 @@ namespace Barotrauma.Items.Components } } - wireSprite = overrideSprite ?? defaultWireSprite; + return overrideSprite ?? defaultWireSprite; + } + + partial void InitProjSpecific(ContentXElement element) + { + wireSprite = ExtractWireSprite(element); + if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { @@ -181,20 +188,20 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } else if (item.IsSelected) { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, item.Color, drawOffset, depth, Width); + section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -239,13 +246,13 @@ namespace Barotrauma.Items.Components } WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.Color, 0.0f, Width); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, item.Color, itemDepth, Width); @@ -255,8 +262,8 @@ namespace Barotrauma.Items.Components else { WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, item.DrawPosition, item.Color, 0.0f, Width); } @@ -294,12 +301,12 @@ namespace Barotrauma.Items.Components Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f; WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, endPos, GUIStyle.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, start + (endPos - start) * 0.7f, item.Color, depth, 0.3f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/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 a11245fe4..5b4d60065 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1603,88 +1603,7 @@ namespace Barotrauma if (itemContainer != null && itemContainer.ShowContainedStateIndicator && itemContainer.Capacity > 0) { - float containedState = 0.0f; - if (itemContainer.ShowConditionInContainedStateIndicator) - { - containedState = item.Condition / item.MaxCondition; - } - else - { - int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0); - ItemSlot containedItemSlot = null; - if (targetSlot < itemContainer.Inventory.slots.Length) - { - containedItemSlot = itemContainer.Inventory.slots[targetSlot]; - } - if (containedItemSlot != null) - { - Item containedItem = containedItemSlot.FirstOrDefault(); - if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) - { - if (containedItem == null) - { - // No item on the defined slot, check if the items on other slots can be used. - containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot)); - } - if (containedItem != null) - { - int ignoredItemCount = 0; - var subContainableItems = itemContainer.AllSubContainableItems; - float capacity = itemContainer.GetMaxStackSize(targetSlot); - if (subContainableItems != null) - { - bool useMainContainerCapacity = true; - foreach (Item it in itemContainer.Inventory.AllItems) - { - // Ignore all items in the sub containers. - foreach (RelatedItem ri in subContainableItems) - { - if (ri.MatchesItem(containedItem)) - { - // The target item is in a subcontainer -> inverse the logic. - useMainContainerCapacity = false; - break; - } - if (ri.MatchesItem(it)) - { - ignoredItemCount++; - } - } - if (!useMainContainerCapacity) { break; } - } - if (useMainContainerCapacity) - { - capacity *= itemContainer.MainContainerCapacity; - } - else - { - // Ignore all items in the main container. - ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity; - } - } - int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount; - containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1); - } - } - else - { - containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - - if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) - { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) - { - containedState = containedItemSlot.Items.Count / (float)maxStackSize; - } - } - } - } - } - + float containedState = itemContainer.GetContainedIndicatorState(); int dir = slot.SubInventoryDir; Rectangle containedIndicatorArea = new Rectangle(rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); @@ -1794,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 && @@ -1807,6 +1735,7 @@ namespace Barotrauma } } + private static void DrawItemStateIndicator( SpriteBatch spriteBatch, Inventory inventory, Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, 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 647e2dbc3..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() @@ -260,16 +270,16 @@ namespace Barotrauma public override void UpdatePlacing(Camera cam) { - Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); if (PlayerInput.SecondaryMouseButtonClicked()) { Selected = null; return; } + + var potentialContainer = MapEntity.GetPotentialContainer(cam.ScreenToWorld(PlayerInput.MousePosition)); - var potentialContainer = MapEntity.GetPotentialContainer(position); - + Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); if (!ResizeHorizontal && !ResizeVertical) { if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 7024d24bb..095195834 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -293,11 +293,11 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f)); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f)); if (GameMain.DebugDraw) { 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/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 2ebbfc897..51fa11094 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -515,11 +515,11 @@ namespace Barotrauma Item targetContainer = null; bool isShiftDown = PlayerInput.IsShiftDown(); - if (!isShiftDown) return null; + if (!isShiftDown) { return null; } foreach (MapEntity e in mapEntityList) { - if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } + if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; } if (e.IsMouseOn(position)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 365db83ac..c229c7011 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -83,7 +83,10 @@ namespace Barotrauma { string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + if (!ContentPackageManager.ModsEnabled) + { + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + } return null; } catch (System.IO.InvalidDataException e) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index a08893601..f15259990 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -89,7 +89,11 @@ namespace Barotrauma CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) + public void CreateSpecsWindow(GUIListBox parent, GUIFont font, + bool includeTitle = true, + bool includeClass = true, + bool includeDescription = false, + bool includeCrushDepth = false) { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; @@ -155,6 +159,22 @@ namespace Barotrauma { CanBeFocused = false }; cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height); + if (includeCrushDepth) + { + var crushDepthText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("CrushDepth"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crushDepthText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.GetWithVariable("meterformat", "[meters]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetSubCrushDepth())), + textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + crushDepthText.RectTransform.MinSize = new Point(0, crushDepthText.Children.First().Rect.Height); + } + if (RecommendedCrewSizeMax > 0) { var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), @@ -227,5 +247,57 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren().Where(c => c != submarineNameText && c != descBlock)); parent.ForceLayoutRecalculation(); } + + public readonly record struct PendingSubInfo(SubmarineInfo PendingSub = null, bool StructuresDefineRealWorldCrushDepth = false, float RealWorldCrushDepth = Level.DefaultRealWorldCrushDepth); + + private float GetSubCrushDepth() + { + var pendingSubInfo = new PendingSubInfo(); + return GetSubCrushDepth(this, ref pendingSubInfo); + } + + public static float GetSubCrushDepth(SubmarineInfo subInfo, ref PendingSubInfo pendingSubInfo) + { + float subCrushDepth = Level.DefaultRealWorldCrushDepth; + if (Submarine.MainSub != null && Submarine.MainSub.Info == subInfo) + { + subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; + } + else if (subInfo != null) + { + if (pendingSubInfo.PendingSub != subInfo) + { + // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time + pendingSubInfo = new PendingSubInfo(subInfo, subInfo.IsCrushDepthDefinedInStructures(out float realWorldCrushDepth), realWorldCrushDepth); + } + subCrushDepth = pendingSubInfo.RealWorldCrushDepth; + } + if (GameMain.GameSession?.Campaign?.UpgradeManager != null && UpgradePrefab.Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab) + { + int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), info: subInfo); + // If there is a sub switch pending, unless its structures have crush depth defined in their elements, + // calculate the value based on the default crush depth and pending upgrade level + int currentLevel = 0; + if (pendingSubInfo.PendingSub is null || pendingSubInfo.StructuresDefineRealWorldCrushDepth) + { + currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevelForSub(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), subInfo); + } + if (pendingLevel > currentLevel) + { + string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); + if (!string.IsNullOrEmpty(updateValueStr)) + { + if (currentLevel > 0) + { + // If the current level is greater than 0, reset the crush depth value back to the base value before calculating the upgrade + int upgradePercentage = UpgradePrefab.ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings: true); + subCrushDepth /= (1f + (upgradePercentage / 100f * currentLevel)); + } + subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel, updateValueStr); + } + } + } + return subCrushDepth; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e547ec854..b3f830140 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -4,26 +4,29 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - class SubmarinePreview : IDisposable + sealed class SubmarinePreview : IDisposable { - private SpriteRecorder spriteRecorder; private readonly SubmarineInfo submarineInfo; + + private SpriteRecorder spriteRecorder; private Camera camera; private Task loadTask; + private (Vector2 Min, Vector2 Max) bounds; + private volatile bool isDisposed; private GUIFrame previewFrame; - private class HullCollection + private sealed class HullCollection { public readonly List Rects; public readonly LocalizedString Name; @@ -42,7 +45,7 @@ namespace Barotrauma } } - private struct Door + private readonly struct Door { public readonly Rectangle Rect; @@ -150,7 +153,9 @@ namespace Barotrauma ScrollBarVisible = false, Spacing = GUI.IntScale(5) }; - subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); + subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, + includeTitle: false, + includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { @@ -186,7 +191,22 @@ namespace Barotrauma }); recalculateSpecsContainerHeight(); - GeneratePreviewMeshes(); + TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => + { + if (isDisposed) { return; } + // Reset the camera's position on the main thread, + // because the Camera class is not thread-safe and + // it's possible for its state to not get updated + // properly if done within a task + camera.Position = (bounds.Min + bounds.Max) * (0.5f, -0.5f); + Vector2 span2d = bounds.Max - bounds.Min; + Vector2 scaledSpan2d = span2d / camera.Resolution.ToVector2(); + float scaledSpan = Math.Max(scaledSpan2d.X, scaledSpan2d.Y); + camera.MinZoom = Math.Min(0.1f, 0.4f / scaledSpan); + camera.Zoom = 0.7f / scaledSpan; + camera.StopMovement(); + camera.UpdateTransform(interpolate: false, updateListener: false); + }); } public static void AddToGUIUpdateList() @@ -207,6 +227,7 @@ namespace Barotrauma spriteRecorder.Begin(SpriteSortMode.BackToFront); HashSet toIgnore = new HashSet(); + HashSet wires = new HashSet(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { @@ -221,7 +242,7 @@ namespace Barotrauma ExtractItemContainerIds(component, toIgnore); break; case "connectionpanel": - ExtractConnectionPanelLinks(component, toIgnore); + ExtractConnectionPanelLinks(component, wires); break; } } @@ -231,20 +252,25 @@ namespace Barotrauma await Task.Yield(); } + var wireNodes = new List(); + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } switch (subElement.Name.LocalName.ToLowerInvariant()) { + case "structure": case "item": - if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) + var id = subElement.GetAttributeInt("ID", 0); + if (wires.Contains(id)) + { + wireNodes.Add(subElement); + } + else if (!toIgnore.Contains(id)) { BakeMapEntity(subElement); } break; - case "structure": - BakeMapEntity(subElement); - break; case "hull": Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) @@ -261,15 +287,14 @@ namespace Barotrauma if (isDisposed) { return; } await Task.Yield(); } - spriteRecorder.End(); - camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; - float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; - camera.Zoom = 0.8f / scaledSpan; - camera.StopMovement(); + bounds = (spriteRecorder.Min, spriteRecorder.Max); + wireNodes.ForEach(BakeWireNodes); + + spriteRecorder.End(); } - private void ExtractItemContainerIds(XElement component, HashSet ids) + private static void ExtractItemContainerIds(XElement component, HashSet ids) { string containedString = component.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); @@ -283,7 +308,7 @@ namespace Barotrauma } } - private void ExtractConnectionPanelLinks(XElement component, HashSet ids) + private static void ExtractConnectionPanelLinks(XElement component, HashSet ids) { var pins = component.Elements("input").Concat(component.Elements("output")); foreach (var pin in pins) @@ -297,6 +322,39 @@ namespace Barotrauma } } + private void BakeWireNodes(XElement element) + { + var prefabIdentifier = element.GetAttributeIdentifier("identifier", ""); + if (prefabIdentifier.IsEmpty) { return; } + if (!ItemPrefab.Prefabs.TryGet(prefabIdentifier, out var prefab)) { return; } + + var prefabWireComponentElement = prefab.ConfigElement.GetChildElement("wire"); + if (prefabWireComponentElement is null) { return; } + + var wireComponent = element.GetChildElement("wire"); + if (wireComponent is null) { return; } + + var color = element.GetAttributeColor("spritecolor") ?? Color.White; + + var nodes = Wire.ExtractNodes(wireComponent).ToImmutableArray(); + var wireSprite = Wire.ExtractWireSprite(prefab.ConfigElement); + + var useSpriteDepth = element.GetAttributeBool("usespritedepth", false); + var depth = + useSpriteDepth + ? element.GetAttributeFloat("spritedepth", 1.0f) + : wireSprite.Depth; + + var width = prefabWireComponentElement.GetAttributeFloat("width", 0.3f); + + for (int i = 0; i < nodes.Length - 1; i++) + { + var line = (Start: nodes[i], End: nodes[i + 1]); + var wireSegment = new Wire.WireSection(line.Start, line.End); + wireSegment.Draw(spriteRecorder, wireSprite, color, Vector2.Zero, depth, width); + } + } + private void BakeMapEntity(XElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); @@ -313,27 +371,27 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = null; - if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && - ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + MapEntityPrefab prefab; + if (element.NameAsIdentifier() == "item" + && ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) { prefab = ip; } else { - prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + prefab = MapEntityPrefab.FindByIdentifier(identifier); } if (prefab == null) { return; } - var texture = prefab.Sprite.Texture; - var srcRect = prefab.Sprite.SourceRect; + flippedX &= prefab.CanSpriteFlipX; + flippedY &= prefab.CanSpriteFlipY; SpriteEffects spriteEffects = SpriteEffects.None; - if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) + if (flippedX) { spriteEffects |= SpriteEffects.FlipHorizontally; } - if (flippedY && ((prefab as ItemPrefab)?.CanSpriteFlipY ?? true)) + if (flippedY) { spriteEffects |= SpriteEffects.FlipVertically; } @@ -419,8 +477,8 @@ namespace Barotrauma { float offsetState = 0f; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + if (flippedX) { offset.X = -offset.X; } + if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteRecorder, new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, @@ -451,8 +509,8 @@ namespace Barotrauma float rotationState = 0f; float offsetState = 0f; float rot = decorativeSprite.GetRotation(ref rotationState, 0f); Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + if (flippedX) { offset.X = -offset.X; } + if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); @@ -472,6 +530,7 @@ namespace Barotrauma { overrideSprite = false; + float relativeScale = scale / prefab.Scale; foreach (var subElement in prefab.ConfigElement.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -498,7 +557,6 @@ namespace Barotrauma relativeBarrelPos, MathHelper.ToRadians(rotation)); - float relativeScale = scale / prefab.Scale; Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); drawPos.Y = -drawPos.Y; @@ -516,20 +574,22 @@ namespace Barotrauma break; case "door": - doors.Add(new Door(rect)); + var scaledRect = rect with { Size = (rect.Size.ToVector2() * relativeScale).ToPoint() }; + + doors.Add(new Door(scaledRect)); var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); if (doorSpriteElem != null) { - string texturePath = doorSpriteElem.GetAttributeString("texture", ""); - Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); + string texturePath = doorSpriteElem.GetAttributeStringUnrestricted("texture", ""); + Vector2 pos = scaledRect.Location.ToVector2() * new Vector2(1f, -1f); if (subElement.GetAttributeBool("horizontal", false)) { - pos.Y += (float)rect.Height * 0.5f; + pos.Y += (float)scaledRect.Height * 0.5f; } else { - pos.X += (float)rect.Width * 0.5f; + pos.X += (float)scaledRect.Width * 0.5f; } Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); spriteRecorder.Draw(doorSprite.Texture, pos, @@ -555,7 +615,7 @@ namespace Barotrauma } } - public void ParseUpgrades(XElement prefabConfigElement, ref float scale) + private void ParseUpgrades(XElement prefabConfigElement, ref float scale) { foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 94e3da6c4..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; @@ -66,9 +66,20 @@ namespace Barotrauma.Networking }; var addressOrAccountId = bannedPlayer.AddressOrAccountId; - GUITextBlock textBlock = new GUITextBlock( - new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), - bannedPlayer.Name + " (" + addressOrAccountId + ")") { CanBeFocused = true }; + + string nameText = bannedPlayer.Name; + if (addressOrAccountId.TryCast(out Address address)) + { + nameText += $" ({address.StringRepresentation})"; + } + else if (addressOrAccountId.TryCast(out AccountId accountId)) + { + nameText += $" ({accountId.StringRepresentation})"; + } + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), nameText) + { + CanBeFocused = true + }; textBlock.RectTransform.MinSize = new Point( (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); @@ -83,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); @@ -106,7 +118,7 @@ namespace Barotrauma.Networking private bool RemoveBan(GUIButton button, object obj) { - if (!(obj is BannedPlayer banned)) { return false; } + if (obj is not BannedPlayer banned) { return false; } localRemovedBans.Add(banned.UniqueIdentifier); RecreateBanFrame(); @@ -138,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/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 89abf5bf8..61bf8cec3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -34,6 +34,7 @@ namespace Barotrauma.Networking catch { DebugConsole.ThrowError($"Failed to start ChildServerRelay Process. File: {processInfo.FileName}, arguments: {processInfo.Arguments}"); + ForceShutDown(); throw; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 98fd63aa1..34449232a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Networking if (GameStarted && Screen.Selected == GameMain.GameScreen) { - EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); + EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned; RespawnManager?.Update(deltaTime); @@ -1102,11 +1102,7 @@ namespace Barotrauma.Networking VoipClient = new VoipClient(this, ClientPeer); //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods - if (!(Screen.Selected is GameScreen) && !(Screen.Selected is RoundSummaryScreen) && !(Screen.Selected is NetLobbyScreen)) - { - GameMain.ModDownloadScreen.Select(); - } - else + if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen) { EntityEventManager.ClearSelf(); foreach (Character c in Character.CharacterList) @@ -1114,6 +1110,10 @@ namespace Barotrauma.Networking c.ResetNetState(); } } + else + { + GameMain.ModDownloadScreen.Select(); + } chatBox.InputBox.Enabled = true; if (GameMain.NetLobbyScreen?.ChatInput != null) @@ -1535,8 +1535,9 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; - DateTime? timeOut = null; + //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message TimeSpan timeOutDuration = new TimeSpan(0, 0, seconds: 30); + DateTime timeOut = DateTime.Now + timeOutDuration; DateTime requestFinalizeTime = DateTime.Now; TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); IWriteMessage msg = new WriteOnlyMessage(); @@ -1545,11 +1546,15 @@ namespace Barotrauma.Networking GUIMessageBox interruptPrompt = null; - while (true) + if (includesFinalize) { - try + ReadStartGameFinalize(inc); + } + else + { + while (true) { - if (timeOut.HasValue) + try { if (DateTime.Now > requestFinalizeTime) { @@ -1583,41 +1588,30 @@ namespace Barotrauma.Networking return true; }; } - } - else - { - if (includesFinalize) + + if (!connected) { - ReadStartGameFinalize(inc); + roundInitStatus = RoundInitStatus.Interrupted; break; } - //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message - timeOut = DateTime.Now + timeOutDuration; + if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } } - - if (!connected) + catch (Exception e) { - roundInitStatus = RoundInitStatus.Interrupted; + DebugConsole.ThrowError("There was an error initializing the round.", e, true); + roundInitStatus = RoundInitStatus.Error; break; } - if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } + //waiting for a STARTGAMEFINALIZE message + yield return CoroutineStatus.Running; } - catch (Exception e) - { - DebugConsole.ThrowError("There was an error initializing the round.", e, true); - roundInitStatus = RoundInitStatus.Error; - break; - } - - //waiting for a STARTGAMEFINALIZE message - yield return CoroutineStatus.Running; } interruptPrompt?.Close(); interruptPrompt = null; - + if (roundInitStatus != RoundInitStatus.Started) { if (roundInitStatus != RoundInitStatus.Interrupted) @@ -1767,7 +1761,7 @@ namespace Barotrauma.Networking { string subName = inc.ReadString(); string subHash = inc.ReadString(); - byte subClass = inc.ReadByte(); + SubmarineClass subClass = (SubmarineClass)inc.ReadByte(); bool isShuttle = inc.ReadBoolean(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); @@ -1776,7 +1770,7 @@ namespace Barotrauma.Networking { matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false) { - SubmarineClass = (SubmarineClass)subClass + SubmarineClass = subClass }; if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); } } @@ -2009,10 +2003,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); GameMain.NetLobbyScreen.SetMissionType(missionType); - if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); + GameMain.NetLobbyScreen.SelectMode(modeIndex); if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); + if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); } } if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) @@ -2101,13 +2095,12 @@ namespace Barotrauma.Networking case ServerNetSegment.EntityPosition: inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); - UInt32 incomingUintIdentifier = inc.ReadUInt32(); - UInt16 id = inc.ReadUInt16(); uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - - var entity = Entity.FindEntityByID(id) as IServerPositionSync; + + var header = INetSerializableStruct.Read(inc); + + var entity = Entity.FindEntityByID(header.EntityId) as IServerPositionSync; if (msgEndPos > inc.LengthBits) { DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); @@ -2117,15 +2110,15 @@ namespace Barotrauma.Networking debugEntityList.Add(entity); if (entity != null) { - if (entity is Item != isItem) + if (entity is Item != header.IsItem) { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(header.IsItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } - else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && - uintIdentifier != incomingUintIdentifier) + else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me && + uintIdentifier != header.PrefabUintIdentifier) { DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." - +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]"}, " +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); } else @@ -2133,7 +2126,6 @@ namespace Barotrauma.Networking entity.ClientReadPosition(inc, sendingTime); } } - //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason inc.BitPosition = msgEndPos; @@ -2144,7 +2136,7 @@ namespace Barotrauma.Networking break; case ServerNetSegment.EntityEvent: case ServerNetSegment.EntityEventInitial: - if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList)) + if (!EntityEventManager.Read(segment, inc, sendingTime)) { return SegmentTableReader.BreakSegmentReading.Yes; } @@ -2413,7 +2405,9 @@ namespace Barotrauma.Networking var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); + var existingSubs = SubmarineInfo.SavedSubmarines + .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash) + .ToList(); foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); @@ -2472,12 +2466,13 @@ namespace Barotrauma.Networking } // Replace a submarine dud with the downloaded version - SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => + s.Name == newSub.Name + && s.MD5Hash == newSub.MD5Hash); if (existingServerSub != null) { int existingIndex = ServerSubmarines.IndexOf(existingServerSub); - ServerSubmarines.RemoveAt(existingIndex); - ServerSubmarines.Insert(existingIndex, newSub); + ServerSubmarines[existingIndex] = newSub; existingServerSub.Dispose(); } @@ -2798,7 +2793,6 @@ namespace Barotrauma.Networking /// public void RequestSelectMode(int modeIndex) { - if (!HasPermission(ClientPermissions.SelectMode)) return; if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren) { DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); @@ -2852,13 +2846,14 @@ namespace Barotrauma.Networking /// /// Tell the server to end the round (permission required) /// - public void RequestRoundEnd(bool save) + public void RequestRoundEnd(bool save, bool quitCampaign = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); msg.WriteBoolean(true); //indicates round end msg.WriteBoolean(save); + msg.WriteBoolean(quitCampaign); ClientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 0d14de93b..3db7e69c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -109,16 +109,15 @@ namespace Barotrauma.Networking private UInt16? firstNewID; + private readonly List tempEntityList = new List(); /// /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// - public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List entities) + public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) { - UInt16 unreceivedEntityEventCount = 0; - if (type == ServerNetSegment.EntityEventInitial) { - unreceivedEntityEventCount = msg.ReadUInt16(); + UInt16 unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); if (GameSettings.CurrentConfig.VerboseLogging) @@ -143,7 +142,7 @@ namespace Barotrauma.Networking } } - entities.Clear(); + tempEntityList.Clear(); msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); @@ -156,9 +155,9 @@ namespace Barotrauma.Networking { string errorMsg = $"Error while reading a message from the server. Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits})."; errorMsg += "\nPrevious entities:"; - for (int j = entities.Count - 1; j >= 0; j--) + for (int j = tempEntityList.Count - 1; j >= 0; j--) { - errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); + errorMsg += "\n" + (tempEntityList[j] == null ? "NULL" : tempEntityList[j].ToString()); } DebugConsole.ThrowError(errorMsg); return false; @@ -174,7 +173,7 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); } - entities.Add(null); + tempEntityList.Add(null); if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } continue; } @@ -182,7 +181,7 @@ namespace Barotrauma.Networking int msgLength = (int)msg.ReadVariableUInt32(); IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable; - entities.Add(entity); + tempEntityList.Add(entity); //skip the event if we've already received it or if the entity isn't found if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null) @@ -223,7 +222,7 @@ namespace Barotrauma.Networking if (msg.BitPosition != msgPosition + msgLength * 8) { - var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; + var prevEntity = tempEntityList.Count >= 2 ? tempEntityList[tempEntityList.Count - 2] : null; ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 2e5976f61..c51b66457 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -30,6 +30,8 @@ namespace Barotrauma.Networking protected readonly bool isOwner; protected readonly Option ownerKey; + public bool IsActive => isActive; + protected bool isActive; public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) @@ -80,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/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index f07d6f7a9..67ba366d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -98,16 +98,15 @@ namespace Barotrauma foreach (GUIComponent comp in listBox.Content.Children) { if (comp.UserData != userData) { continue; } - if (!(comp.FindChild("votes") is GUITextBlock voteText)) + if (comp.FindChild("votes") is not GUITextBlock voteText) { - voteText = new GUITextBlock(new RectTransform(new Point(30, comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), - "", textAlignment: Alignment.CenterRight) + voteText = new GUITextBlock(new RectTransform(new Point(GUI.IntScale(30), comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), + "", textAlignment: Alignment.Center) { Padding = Vector4.Zero, UserData = "votes" }; } - voteText.Text = votes == 0 ? "" : votes.ToString(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 5083a49c3..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; @@ -303,7 +323,7 @@ namespace Barotrauma bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } int hiddenOptions = 0; @@ -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 1b8f74e3c..a3ae05b14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -365,7 +365,7 @@ namespace Barotrauma private void CreateCustomizeWindow(CampaignSettings prevSettings, Action onClosed = null) { - CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); + CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.5f), minSize: new Point(450, 350)); GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); @@ -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 6eb0b00ab..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; } } @@ -125,7 +122,7 @@ namespace Barotrauma.CharacterEditor { ResetVariables(); var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); - Submarine.MainSub = new Submarine(subInfo); + Submarine.MainSub = new Submarine(subInfo, showErrorMessages: false); if (Submarine.MainSub.PhysicsBody != null) { Submarine.MainSub.PhysicsBody.Enabled = false; @@ -162,11 +159,6 @@ namespace Barotrauma.CharacterEditor OpenDoors(); GameMain.Instance.ResolutionChanged += OnResolutionChanged; Instance = this; - - if (!GameSettings.CurrentConfig.EditorDisclaimerShown) - { - GameMain.Instance.ShowEditorDisclaimer(); - } } private void ResetVariables() @@ -267,7 +259,10 @@ namespace Barotrauma.CharacterEditor #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; - GameMain.LightManager.LightingEnabled = true; + if (!GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = true; + } ClearWidgets(); ClearSelection(); } @@ -285,6 +280,7 @@ namespace Barotrauma.CharacterEditor #region Main methods public override void AddToGUIUpdateList() { + if (rightArea == null || leftArea == null) { return; } rightArea.AddToGUIUpdateList(); leftArea.AddToGUIUpdateList(); @@ -783,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) @@ -1575,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; } @@ -2688,10 +2681,6 @@ namespace Barotrauma.CharacterEditor // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.2f, 0.7f), characterLabel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") - { - OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } - }; var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { @@ -4007,7 +3996,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { @@ -4116,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/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 620f10795..aee1762ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -820,6 +820,15 @@ namespace Barotrauma }; valueInput.Text = newValue?.ToString() ?? ""; } + else if (type == typeof(Identifier)) + { + GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? string.Empty); + valueInput.OnTextChanged += (component, o) => + { + newValue = new Identifier(o); + return true; + }; + } else if (type == typeof(float) || type == typeof(int)) { GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; 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 93e9a68ff..107c22358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -873,7 +873,25 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; + string fileName; + if (serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f && + f.ContentPackage != GameMain.VanillaContent) + { + fileName = Path.Combine( + Path.GetDirectoryName(f.Path.Value), + Path.GetFileNameWithoutExtension(f.Path.Value)); +#if WINDOWS + fileName += ".exe"; +#endif + } + else + { +#if WINDOWS + fileName = "DedicatedServer.exe"; +#else + fileName = "./DedicatedServer"; +#endif + } string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -897,19 +915,10 @@ namespace Barotrauma } int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments += " -ownerkey " + ownerKey; - - string filename = Path.Combine( - Path.GetDirectoryName(exeName), - Path.GetFileNameWithoutExtension(exeName)); -#if WINDOWS - filename += ".exe"; -#else - filename = "./" + exeName; -#endif - + var processInfo = new ProcessStartInfo { - FileName = filename, + FileName = fileName, Arguments = arguments, WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG @@ -985,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/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index c6956035d..59514c304 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -189,7 +189,8 @@ namespace Barotrauma } public IReadOnlyList GetSubList() - => SubList.Content.Children.Select(c => c.UserData as SubmarineInfo).ToArray(); + => (IReadOnlyList)GameMain.Client?.ServerSubmarines + ?? Array.Empty(); public readonly GUIListBox PlayerList; @@ -929,6 +930,8 @@ namespace Barotrauma var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); + //leave some padding for the vote count text + modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; @@ -981,7 +984,7 @@ namespace Barotrauma } else { - GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); } return true; } @@ -3291,16 +3294,15 @@ namespace Barotrauma { //campaign running settingsBlocker.Visible = true; - CampaignFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); - ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageRound)); - QuitCampaignButton.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); + ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible; CampaignSetupFrame.Visible = false; } else { CampaignFrame.Visible = false; CampaignSetupFrame.Visible = true; - if (!GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)) + if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound)) { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), @@ -3364,7 +3366,7 @@ namespace Barotrauma CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } RefreshEnabledElements(); - if (enabled) + if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 0d77adef9..3e95da567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -543,13 +543,6 @@ namespace Barotrauma } }; - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), paddedTopPanel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } - }; - disclaimerBtn.RectTransform.MaxSize = new Point(disclaimerBtn.Rect.Height); - TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y)); paddedTopPanel.Recalculate(); @@ -1425,7 +1418,7 @@ namespace Barotrauma else if (MainSub == null) { var subInfo = new SubmarineInfo(); - MainSub = new Submarine(subInfo); + MainSub = new Submarine(subInfo, showErrorMessages: false); } MainSub.UpdateTransform(interpolate: false); @@ -1462,11 +1455,6 @@ namespace Barotrauma ImageManager.OnEditorSelected(); ReconstructLayers(); - - if (!GameSettings.CurrentConfig.EditorDisclaimerShown) - { - GameMain.Instance.ShowEditorDisclaimer(); - } } public override void OnFileDropped(string filePath, string extension) @@ -2726,11 +2714,13 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true); GUIButton createTabberBtn(string labelTag) { var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.TextBlock.Wrap = true; + btn.TextBlock.SetTextPos(); btn.RectTransform.MaxSize = RectTransform.MaxPoint; btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); btn.Font = GUIStyle.SmallFont; @@ -5635,8 +5625,7 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) - && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) + if ((GUI.MouseOn == null || !GUI.MouseOn.IsChildOf(TopPanel)) && dummyCharacter?.SelectedItem == null && !WiringMode && (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero)) { 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/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 835981330..575e822eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -648,6 +648,12 @@ namespace Barotrauma.Sounds if (isConnected == 0) { + if (!GameMain.Instance.HasLoaded) + { + //wait for loading to finish so we don't start releasing and reloading sounds when they're being loaded, + //or throw an error mid-loading that'd prevent the content package from being enabled + return; + } DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); SetAudioOutputDevice(""); Disconnected = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index bb5f74966..64ba580ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -865,17 +865,18 @@ namespace Barotrauma public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { + var suitableSounds = damageSounds.Where(s => + s.DamageType == damageType && + (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); + //if the damage is too low for any sound, don't play anything - if (damageSounds.All(d => damage < d.DamageRange.X)) { return; } + if (suitableSounds.All(d => damage < d.DamageRange.X)) { return; } //allow the damage to differ by 10 from the configured damage range, //so the same amount of damage doesn't always play the same sound float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); - - var suitableSounds = damageSounds.Where(s => - s.DamageType == damageType && - (s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)) && - (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); + suitableSounds = suitableSounds.Where(s => + s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)); var damageSound = suitableSounds.GetRandomUnsynced(); damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); 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/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index c3d167fed..e3de26b71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -218,6 +218,20 @@ namespace Barotrauma.Steam var descriptionTextBox = ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty); + if (workshopItem.Id != 0) + { + TaskPool.Add( + $"GetFullDescription{workshopItem.Id}", + SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? itemWithDescription)) { return; } + + descriptionTextBox.Text = itemWithDescription?.Description ?? descriptionTextBox.Text; + descriptionTextBox.Deselect(); + }); + } + var (leftBottom, _, rightBottom) = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f); leftBottom.Stretch = true; 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/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs index 7443204bb..784421558 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs @@ -2,7 +2,7 @@ namespace Barotrauma { - partial class UpgradePrefab + sealed partial class UpgradePrefab { public readonly ImmutableArray DecorativeSprites = new ImmutableArray(); public Sprite Sprite { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs new file mode 100644 index 000000000..5a34c929f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework.Graphics; +namespace Barotrauma; + +static class EffectLoader +{ + public static Effect Load(string path) + => GameMain.Instance.Content.Load(path +#if LINUX || OSX + +"_opengl" +#endif + ); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index f1fc1456d..45111625f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -23,36 +23,55 @@ namespace Barotrauma public static void ConvertMasterLocalizationKit(string outputTextsDirectory, string outputConversationsDirectory, bool convertConversations) { - string textFilePath = Path.Combine(infoTextPath, "Texts.csv"); - string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); + List languages = new List(); + for (int i = 0; i < 2; i++) + { + string textFilePath; + string outputFileName; + switch (i) + { + case 0: + textFilePath = Path.Combine(infoTextPath, "Texts.csv"); + outputFileName = "Vanilla.xml"; + break; + case 1: + textFilePath = Path.Combine(infoTextPath, "EditorTexts.csv"); + outputFileName = "VanillaEditorTexts.xml"; + break; + default: + throw new NotImplementedException(); + } - Dictionary> xmlContent; - try - { - xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); - } - catch (Exception e) - { - DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); - return; - } - if (xmlContent == null) - { - DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); - return; - } - foreach (string language in xmlContent.Keys) - { - string languageNoWhitespace = language.Replace(" ", ""); - string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}Vanilla.xml"); - File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); - DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + Dictionary> xmlContent; + try + { + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); + } + catch (Exception e) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); + return; + } + if (xmlContent == null) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); + return; + } + foreach (string language in xmlContent.Keys) + { + languages.Add(language); + string languageNoWhitespace = language.Replace(" ", ""); + string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}{outputFileName}"); + File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); + DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + } } if (convertConversations) { + string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); var conversationLinesAll = File.ReadAllLines(conversationFilePath, Encoding.UTF8); - foreach (string language in xmlContent.Keys) + foreach (string language in languages) { List convXmlContent = ConvertConversationsToXML(conversationLinesAll, language); if (convXmlContent == null) @@ -61,7 +80,7 @@ namespace Barotrauma continue; } string languageNoWhitespace = language.Replace(" ", ""); - string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"NpcConversations_{languageNoWhitespace}.xml"); + string xmlFileFullPath = Path.Combine(outputConversationsDirectory, languageNoWhitespace, $"NpcConversations_{languageNoWhitespace}.xml"); File.WriteAllLines(xmlFileFullPath, convXmlContent, Encoding.UTF8); DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath); } @@ -339,7 +358,8 @@ namespace Barotrauma string[] headerSplit = csvContent[0].Split(separator); for (int i = 0; i < headerSplit.Length; i++) { - if (headerSplit[i] == language) + if (headerSplit[i] == language || + (language == "English" && headerSplit[i]== "Line (Original)")) { languageColumn = i; break; @@ -348,6 +368,7 @@ namespace Barotrauma xmlContent.Add($""); + conversationClosingIndent.Clear(); int conversationStart = 1; xmlContent.Add(string.Empty); @@ -399,9 +420,9 @@ namespace Barotrauma { string[] nextConversationElement = csvContent[i + 1].Split(separator); - if (nextConversationElement[1] != string.Empty) + if (nextConversationElement[3] != string.Empty) { - nextDepth = int.Parse(nextConversationElement[2]); + nextDepth = int.Parse(nextConversationElement[3]); nextIsSubConvo = nextDepth > depthIndex; } @@ -421,7 +442,12 @@ namespace Barotrauma } else { + //end of file, close remaining xml tags xmlContent.Add(element.TrimEnd() + "/>"); + for (int j = depthIndex - 1; j >= 0; j--) + { + HandleClosingElements(xmlContent, j); + } } } @@ -433,12 +459,12 @@ namespace Barotrauma private static void HandleClosingElements(List xmlContent, int targetDepth) { - if (conversationClosingIndent.Count == 0) return; + if (conversationClosingIndent.Count == 0) { return; } for (int k = conversationClosingIndent.Count - 1; k >= 0; k--) { int currentIndent = conversationClosingIndent[k]; - if (currentIndent < targetDepth) break; + if (currentIndent < targetDepth) { break; } xmlContent.Add($"{GetIndenting(currentIndent)}"); conversationClosingIndent.RemoveAt(k); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index 43ba00158..f7c0b35fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -7,28 +7,30 @@ using System.Text; namespace Barotrauma { - class SpriteRecorder : ISpriteBatch, IDisposable + sealed class SpriteRecorder : ISpriteBatch, IDisposable { - private struct Command + public readonly record struct Command( + Texture2D Texture, + VertexPositionColorTexture VertexBL, + VertexPositionColorTexture VertexBR, + VertexPositionColorTexture VertexTL, + VertexPositionColorTexture VertexTR, + float Depth, + Vector2 Min, + Vector2 Max, + int Index) { - public readonly Texture2D Texture; - public readonly VertexPositionColorTexture VertexBL; - public readonly VertexPositionColorTexture VertexBR; - public readonly VertexPositionColorTexture VertexTL; - public readonly VertexPositionColorTexture VertexTR; - public readonly float Depth; - public readonly Vector2 Min; - public readonly Vector2 Max; - public readonly int Index; - - public bool Overlaps(Command other) - { - return - Min.X <= other.Max.X && Max.X >= other.Min.X && - Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; - } - - public Command( + public static Vector2 GetMinPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Min(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Min(vertices.Select(v => v.Position.Y).ToArray())); + + public static Vector2 GetMaxPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Max(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Max(vertices.Select(v => v.Position.Y).ToArray())); + + public static Command FromTransform( Texture2D texture, Vector2 pos, Rectangle srcRect, @@ -46,15 +48,11 @@ namespace Barotrauma int srcRectBottom = srcRect.Bottom; if (effects.HasFlag(SpriteEffects.FlipHorizontally)) { - var temp = srcRectRight; - srcRectRight = srcRectLeft; - srcRectLeft = temp; + (srcRectRight, srcRectLeft) = (srcRectLeft, srcRectRight); } if (effects.HasFlag(SpriteEffects.FlipVertically)) { - var temp = srcRectBottom; - srcRectBottom = srcRectTop; - srcRectTop = temp; + (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } rotation = MathHelper.ToRadians(rotation); @@ -68,59 +66,63 @@ namespace Barotrauma pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; - Texture = texture; + var vertexTl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X, pos.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - Depth = depth; + var vertexTr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - VertexTL.Color = color; - VertexTR.Color = color; - VertexBL.Color = color; - VertexBR.Color = color; + var vertexBl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); - VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); - VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); - VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); + var vertexBr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - Min = new Vector2( - MathUtils.Min - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Min - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var min = GetMinPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - Max = new Vector2( - MathUtils.Max - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Max - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var max = GetMaxPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - - Index = index; + return new Command( + texture, + vertexBl, + vertexBr, + vertexTl, + vertexTr, + depth, + min, + max, + index); + } + public bool Overlaps(Command other) + { + return + Min.X <= other.Max.X && Max.X >= other.Min.X && + Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; } } @@ -151,8 +153,8 @@ namespace Barotrauma public static BasicEffect BasicEffect = null; - private List recordedBuffers = new List(); - private List commandList = new List(); + private readonly List recordedBuffers = new List(); + private readonly List commandList = new List(); private SpriteSortMode currentSortMode; private IndexBuffer indexBuffer = null; @@ -170,16 +172,45 @@ namespace Barotrauma currentSortMode = sortMode; } - public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + private void AppendCommand(Command command) { if (isDisposed) { return; } - - Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0); + if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); - commandList?.Add(command); + commandList.Add(command); + } + + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + { + if (isDisposed) { return; } + + var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count); + AppendCommand(command); + } + + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) + { + if (isDisposed) { return; } + + int iters = count ?? (vertices.Length / 4); + for (int i=0;iY}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 bda6bf5ac..5c27ba425 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.16.1 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 23448ee31..aec4b86f1 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.16.1 + 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 9bcc274e7..adc6b7153 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.16.1 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 618ae1641..5ee5b2bc2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.16.1 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index e66c9475e..6692e0d12 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.16.1 + 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/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index eedb1f10b..b960f2f7d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -8,8 +8,9 @@ namespace Barotrauma { partial class Character { - public Address OwnerClientAddress; - public string OwnerClientName; + private Address ownerClientAddress; + private Option ownerClientAccountId; + public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -19,6 +20,35 @@ namespace Barotrauma public bool HealthUpdatePending; + public void SetOwnerClient(Client client) + { + if (client == null) + { + ownerClientAddress = null; + ownerClientAccountId = Option.None(); + IsRemotePlayer = false; + } + else + { + ownerClientAddress = client.Connection.Endpoint.Address; + ownerClientAccountId = client.AccountId; + IsRemotePlayer = true; + } + } + + public bool IsClientOwner(Client client) + { + if (ownerClientAccountId.TryUnwrap(out var accountId) + && client.AccountId.TryUnwrap(out var clientId)) + { + return accountId == clientId; + } + else + { + return ownerClientAddress == client.Connection.Endpoint.Address; + } + } + public float GetPositionUpdateInterval(Client recipient) { if (!Enabled) { return 1000.0f; } @@ -302,12 +332,8 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - - IWriteMessage tempBuffer = new WriteOnlyMessage(); - if (this == c.Character) { tempBuffer.WriteBoolean(true); @@ -405,11 +431,6 @@ namespace Barotrauma AIController?.ServerWrite(tempBuffer); HealthUpdatePending = false; } - - tempBuffer.WritePadBits(); - - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs new file mode 100644 index 000000000..5842632f7 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal static class HealingCooldown + { + private static readonly Dictionary HealingCooldowns = new(); + + // Little bit less than client's 0.5 second cooldown to account for latency + private const float CooldownDuration = 0.4f; + + public static bool IsOnCooldown(Client client) + { + RemoveExpiredCooldowns(); + return HealingCooldowns.ContainsKey(client); + } + + public static void SetCooldown(Client client) + { + RemoveExpiredCooldowns(); + DateTimeOffset newCooldown = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); + HealingCooldowns[client] = newCooldown; + } + + private static void RemoveExpiredCooldowns() + { + HashSet? expiredCooldowns = null; + + DateTimeOffset now = DateTimeOffset.UtcNow; + + foreach (var (client, cooldown) in HealingCooldowns) + { + if (now < cooldown) { continue; } + + expiredCooldowns ??= new HashSet(); + expiredCooldowns.Add(client); + } + + if (expiredCooldowns is null) { return; } + + foreach (Client expiredCooldown in expiredCooldowns) + { + HealingCooldowns.Remove(expiredCooldown); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 9749fec03..9379178b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1374,7 +1374,7 @@ namespace Barotrauma MultiPlayerCampaign.StartCampaignSetup(); return; } - if (!GameMain.Server.StartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } + if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } } })); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 40f8f9f24..ea3362d68 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -66,6 +66,7 @@ namespace Barotrauma public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; + public readonly string[] CommandLineArgs; public GameMain(string[] args) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 4283c7206..f3d53216a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -27,12 +27,9 @@ namespace Barotrauma AnyOneAllowedToManageCampaign(permissions); } - public bool AllowedToManageWallets(Client client) + public static bool AllowedToManageWallets(Client client) { - return - client.HasPermission(ClientPermissions.ManageCampaign) || - client.HasPermission(ClientPermissions.ManageMoney) || - IsOwner(client); + return AllowedToManageCampaign(client, ClientPermissions.ManageMoney); } public override void ShowStartMessage() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 138dc3cfe..d99340d6b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -109,15 +109,22 @@ namespace Barotrauma return AccountId == other.AccountId && other.ClientAddress == ClientAddress; } + public void Reset() + { + itemData = null; + healthData = null; + WalletData = null; + } + public void SpawnInventoryItems(Character character, Inventory inventory) { if (character == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items. Character was null."); + throw new InvalidOperationException($"Failed to spawn inventory items. Character was null."); } if (itemData == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); + throw new InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); } character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6d879caf7..82810135f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -146,7 +146,7 @@ namespace Barotrauma { NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData; MirrorLevel = false; - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); } public static void StartCampaignSetup() @@ -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(); + } } } @@ -395,7 +401,7 @@ namespace Barotrauma yield return new WaitForSeconds(EndTransitionDuration * 0.5f); } - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); yield return CoroutineStatus.Success; } 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 3dc30f6cf..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); } @@ -348,15 +350,9 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - - IWriteMessage tempBuffer = new WriteOnlyMessage(); body.ServerWrite(tempBuffer); - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - msg.WritePadBits(); } public void CreateServerEvent(T ic) where T : ItemComponent, IServerSerializable @@ -378,7 +374,7 @@ namespace Barotrauma if (!components.Contains(ic)) { return; } var eventData = new ComponentStateEventData(ic, extraData); - if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false."); } GameMain.Server.CreateEntityEvent(this, eventData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 572ffaab8..2f2a1e174 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -5,14 +5,9 @@ namespace Barotrauma { partial class Submarine { - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - IWriteMessage tempBuffer = new WriteOnlyMessage(); subBody.Body.ServerWrite(tempBuffer); - msg.WriteByte((byte)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - msg.WritePadBits(); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) 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 7f33ad6b8..8c9a90c95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -58,6 +58,7 @@ namespace Barotrauma.Networking private DateTime roundStartTime; + private bool wasReadyToStartAutomatically; private bool autoRestartTimerRunning; private float endRoundTimer; @@ -139,6 +140,7 @@ namespace Barotrauma.Networking ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); KarmaManager.SelectPreset(ServerSettings.KarmaPreset); ServerSettings.SetPassword(password); + ServerSettings.SaveSettings(); Voting = new Voting(); @@ -366,8 +368,7 @@ namespace Barotrauma.Networking character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); - + Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); @@ -504,8 +505,7 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && - (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) + else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame) { if (ServerSettings.AutoRestart) { @@ -526,18 +526,25 @@ namespace Barotrauma.Networking } } + bool readyToStartAutomatically = false; if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) { - StartGame(); + readyToStartAutomatically = true; } else if (ServerSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) { - StartGame(); + readyToStartAutomatically = true; } } + if (readyToStartAutomatically) + { + if (!wasReadyToStartAutomatically) { GameMain.NetLobbyScreen.LastUpdateID++; } + TryStartGame(); + } + wasReadyToStartAutomatically = readyToStartAutomatically; } for (int i = disconnectedClients.Count - 1; i >= 0; i--) @@ -763,7 +770,7 @@ namespace Barotrauma.Networking else { string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); @@ -779,7 +786,10 @@ namespace Barotrauma.Networking SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) + { + MultiPlayerCampaign.LoadCampaign(saveName); + } } break; case ClientPacketHeader.VOICE: @@ -1389,13 +1399,13 @@ namespace Barotrauma.Networking if (end) { if (mpCampaign == null || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) + CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound)) { bool save = inc.ReadBoolean(); + bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); @@ -1409,6 +1419,14 @@ namespace Barotrauma.Networking } EndGame(wasSaved: save); } + else if (mpCampaign != null) + { + Log($"Client \"{ClientLogName(sender)}\" quit the currently active campaign.", ServerLog.MessageType.ServerMessage); + GameMain.GameSession = null; + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + + } } } else @@ -1425,12 +1443,11 @@ namespace Barotrauma.Networking { MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); } - } else if (!GameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - StartGame(); + TryStartGame(); } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { @@ -1492,20 +1509,10 @@ namespace Barotrauma.Networking case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; - Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); - - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") + Log("Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ?? "none"), ServerLog.MessageType.ServerMessage); + if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign) { - const int MaxSaves = 255; - var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); - for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) - { - msg.WriteNetSerializableStruct(saveInfos[i]); - } - serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); + TrySendCampaignSetupInfo(sender); } break; case ClientPermissions.ManageCampaign: @@ -1612,7 +1619,7 @@ namespace Barotrauma.Networking { if (GameSettings.CurrentConfig.VerboseLogging) { - DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); + DebugConsole.NewMessage($"Sending initial lobby update to {c.Name}", Color.Gray); } outmsg.WriteByte(c.SessionId); @@ -1745,9 +1752,9 @@ namespace Barotrauma.Networking continue; } - IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); - tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); + var tempBuffer = new ReadWriteMessage(); + var entityPositionHeader = EntityPositionHeader.FromEntity(entity); + tempBuffer.WriteNetSerializableStruct(entityPositionHeader); entityPositionSync.ServerWritePosition(tempBuffer, c); //no more room in this packet @@ -1758,6 +1765,7 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.EntityPosition); outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + outmsg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); @@ -1934,6 +1942,13 @@ namespace Barotrauma.Networking { outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); } + + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign && + connectedClients.None(c => c.Connection == OwnerConnection || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign))) + { + //if no-one has permissions to manage the campaign, show the setup UI to everyone + TrySendCampaignSetupInfo(c); + } } else { @@ -1943,9 +1958,8 @@ namespace Barotrauma.Networking settingsBytes = outmsg.LengthBytes - settingsBytes; int campaignBytes = outmsg.LengthBytes; - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.WriteBoolean(true); outmsg.WritePadBits(); @@ -2023,7 +2037,7 @@ namespace Barotrauma.Networking } } - private void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) + private static void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2037,10 +2051,26 @@ namespace Barotrauma.Networking } } - public bool StartGame() + public bool TryStartGame() { if (initiatedStartGame || GameStarted) { return false; } + GameModePreset selectedMode = + Voting.HighestVoted(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode; + if (selectedMode == null) + { + return false; + } + if (selectedMode == GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is not MultiPlayerCampaign) + { + //DebugConsole.ThrowError($"{nameof(TryStartGame)} failed. Cannot start a multiplayer campaign via {nameof(TryStartGame)} - use {nameof(MultiPlayerCampaign.StartNewCampaign)} or {nameof(MultiPlayerCampaign.LoadCampaign)} instead."); + if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.MultiPlayerCampaign.Identifier; + } + return false; + } + Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; @@ -2060,23 +2090,13 @@ namespace Barotrauma.Networking return false; } - GameModePreset selectedMode = Voting.HighestVoted(VoteType.Mode, connectedClients); - if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; } - if (selectedMode == null) - { - return false; - } - if (selectedMode == GameModePreset.MultiPlayerCampaign && !(GameMain.GameSession?.GameMode is CampaignMode)) - { - DebugConsole.ThrowError("StartGame failed. Cannot start a multiplayer campaign via StartGame - use MultiPlayerCampaign.StartNewCampaign or MultiPlayerCampaign.LoadCampaign instead."); - return false; - } initiatedStartGame = true; - startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); + startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); return true; } + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -2168,7 +2188,6 @@ namespace Barotrauma.Networking initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true }; } - List playingClients = new List(connectedClients); if (ServerSettings.AllowSpectating) { @@ -2413,8 +2432,7 @@ namespace Barotrauma.Networking mpCampaign.ClearSavedExperiencePoints(teamClients[i]); } - spawnedCharacter.OwnerClientAddress = teamClients[i].Connection.Endpoint.Address; - spawnedCharacter.OwnerClientName = teamClients[i].Name; + spawnedCharacter.SetOwnerClient(teamClients[i]); } for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) @@ -2479,7 +2497,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); GameMain.GameScreen.Select(); @@ -2507,14 +2525,11 @@ namespace Barotrauma.Networking private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) { - MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.STARTGAME); msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); - bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); @@ -2530,7 +2545,7 @@ namespace Barotrauma.Networking ServerSettings.WriteMonsterEnabled(msg); - if (campaign == null) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { msg.WriteString(levelSeed); msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); @@ -2566,6 +2581,23 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } + private bool TrySendCampaignSetupInfo(Client client) + { + if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } + + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + { + msg.WriteNetSerializableStruct(saveInfos[i]); + } + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + return true; + } + private bool IsUsingRespawnShuttle() { return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); @@ -3196,16 +3228,16 @@ namespace Barotrauma.Networking } //too far to hear the msg -> don't send - if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; + if (string.IsNullOrWhiteSpace(modifiedMessage)) { continue; } } break; case ChatMessageType.Dead: //character still alive -> don't send - if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; + if (client != senderClient && client.Character != null && !client.Character.IsDead) { continue; } break; case ChatMessageType.Private: //private msg sent to someone else than this client -> don't send - if (client != targetClient && client != senderClient) continue; + if (client != targetClient && client != senderClient) { continue; } break; } @@ -3241,11 +3273,17 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio)) + { + //send to chat-linked wifi components + Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item); + senderRadio.TransmitSignal(s, sentFromChat: true); + } } } @@ -3521,9 +3559,7 @@ namespace Barotrauma.Networking //the client's previous character is no longer a remote player if (client.Character != null) { - client.Character.IsRemotePlayer = false; - client.Character.OwnerClientAddress = null; - client.Character.OwnerClientName = null; + client.Character.SetOwnerClient(null); } if (newCharacter == null) @@ -3549,9 +3585,7 @@ namespace Barotrauma.Networking newCharacter.Info.Character = newCharacter; } - newCharacter.OwnerClientAddress = client.Connection.Endpoint.Address; - newCharacter.OwnerClientName = client.Name; - newCharacter.IsRemotePlayer = true; + newCharacter.SetOwnerClient(client); newCharacter.Enabled = true; client.Character = newCharacter; CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); 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/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 4f93198d9..17d19a55f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -439,8 +439,7 @@ namespace Barotrauma.Networking } clients[i].Character = character; - character.OwnerClientAddress = clients[i].Connection.Endpoint.Address; - character.OwnerClientName = clients[i].Name; + character.SetOwnerClient(clients[i]); GameServer.Log( $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index be597c2da..3eaee94aa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -117,13 +117,13 @@ namespace Barotrauma public void StopSubmarineVote(bool passed) { - if (!(ActiveVote is SubmarineVote)) { return; } + if (ActiveVote is not SubmarineVote) { return; } StopActiveVote(passed); } public void StopMoneyTransferVote(bool passed) { - if (!(ActiveVote is TransferVote)) { return; } + if (ActiveVote is not TransferVote) { return; } StopActiveVote(passed); } @@ -155,7 +155,7 @@ namespace Barotrauma GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } - private void StartOrEnqueueVote(IVote vote) + private static void StartOrEnqueueVote(IVote vote) { if (ActiveVote == null) { @@ -198,9 +198,9 @@ namespace Barotrauma ActiveVote.Timer += deltaTime; - if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout || inGameClients.Count() == 1) { - var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); // Do not take unanswered into account for total @@ -216,7 +216,7 @@ namespace Barotrauma } } - public void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) + public static void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) { foreach (Client client in connectedClients) { @@ -254,7 +254,14 @@ namespace Barotrauma string modeIdentifier = inc.ReadString(); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); if (mode == null || !mode.Votable) { break; } + var prevHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); sender.SetVote(voteType, mode); + var newHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); + if (prevHighestVoted != newHighestVoted) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = mode.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + } break; case VoteType.EndRound: if (!sender.HasSpawned) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index ec2ca5d40..eb1a00ee9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -59,6 +59,7 @@ namespace Barotrauma get { return GameModes[SelectedModeIndex].Identifier; } set { + if (SelectedModeIdentifier == value) { return; } for (int i = 0; i < GameModes.Length; i++) { if (GameModes[i].Identifier == value) @@ -127,9 +128,11 @@ namespace Barotrauma { LevelSeed = ToolBox.RandomSeed(8); - subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)).ToList(); + subs = SubmarineInfo.SavedSubmarines + .Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)) + .ToList(); - if (subs == null || subs.Count() == 0) + if (subs == null || subs.Count == 0) { throw new Exception("No submarines are available."); } @@ -156,7 +159,7 @@ namespace Barotrauma GameModes = GameModePreset.List.ToArray(); } - private List subs; + private readonly List subs; public IReadOnlyList GetSubList() => subs; public string LevelSeed @@ -192,7 +195,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 1313a7ecd..8e904d0d8 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.16.1 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index 40605ddf4..ef782ae23 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -11,6 +11,7 @@ RadiationEnabled="false" StartingBalanceAmount="High" StartItemSet="easy" + MaxMissionCount="3" Difficulty="Easy"/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 6d0e43c18..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,6 +361,10 @@ namespace Barotrauma { targetingTag = "owner"; } + else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) + { + targetingTag = "hostile"; + } else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) { targetingTag = tP.Tag; @@ -365,7 +375,7 @@ namespace Barotrauma { targetingTag = "husk"; } - else if (!Character.IsFriendly(targetCharacter)) + else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { @@ -677,22 +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 the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState - //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it - //TODO: a way for pets to differentiate hostile and friendly humans? - if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter)) + Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character; + if (attacker?.AiTarget != null) { - // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); State = AIState.Attack; @@ -1501,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; } @@ -2315,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 32ce66986..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 @@ -1514,9 +1514,18 @@ namespace Barotrauma startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f); //do a raycast upwards to find any walls - float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; + if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider)) + { + mainCollider = Character.AnimController.Collider; + } + float margin = 0.1f; + if (shouldCrouch) + { + margin *= 2; + } + float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin; - shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null; + shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null; } public bool AllowCampaignInteraction() @@ -1558,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(); @@ -1589,7 +1598,27 @@ namespace Barotrauma (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + foreach (var item in matchingItems) + { + if (item == null) { continue; } + + if (containedTag.IsEmpty || item.OwnInventory == null) + { + //no contained items required, this item's ok + return true; + } + var suitableSlot = item.GetComponent().FindSuitableSubContainerIndex(containedTag); + if (suitableSlot == null) + { + //no restrictions on the suitable slot + return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage); + } + else + { + return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value)); + } + } + return false; } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -2016,11 +2045,9 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool friendlyTeam = IsOnFriendlyTeam(me, other); - bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; + bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } - bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); - if (!speciesGood) { return false; } + if (!me.IsSameSpeciesOrGroup(other)) { return false; } if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; @@ -2029,30 +2056,14 @@ namespace Barotrauma return false; } } + if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) + { + // Hostile NPCs are hostile to all pets, unless they are in the same team. + return false; + } return true; } - public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) - { - if (myTeam == otherTeam) { return true; } - - switch (myTeam) - { - case CharacterTeamType.None: - case CharacterTeamType.Team1: - case CharacterTeamType.Team2: - // Only friendly to the same team and friendly NPCs - return otherTeam == CharacterTeamType.FriendlyNPC; - case CharacterTeamType.FriendlyNPC: - // Friendly NPCs are friendly to both teams - return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2; - default: - return true; - } - } - - public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); - public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) 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 a9ae4cbf7..08d7ea70c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -98,7 +98,7 @@ namespace Barotrauma int containedItemCount = 0; foreach (Item it in container.Inventory.AllItems) { - if (CheckItem(it)) + if (CheckItem(it) && IsInTargetSlot(it)) { containedItemCount++; } @@ -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/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 0c3ae42a1..a87e5cd61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,5 +1,5 @@ -using Barotrauma.Items.Components; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -18,6 +18,7 @@ namespace Barotrauma private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; private Item targetItem; + private int? oxygenSourceSlotIndex; public const float MIN_OXYGEN = 10; @@ -43,12 +44,15 @@ namespace Barotrauma Abandon = true; return; } - targetItem = character.Inventory.FindItemByTag(gearTag, true); + + TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) { - targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true); + TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); } - if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) + if (targetItem == null || + !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && + targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) { TryAddSubObjective(ref getDivingGear, () => { @@ -84,7 +88,7 @@ namespace Barotrauma else { float min = GetMinOxygen(character); - if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) + if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it))) { TryAddSubObjective(ref getOxygen, () => { @@ -93,7 +97,7 @@ namespace Barotrauma if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); - if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) + if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1) { character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } @@ -109,7 +113,8 @@ namespace Barotrauma AllowToFindDivingGear = false, AllowDangerousPressure = true, ConditionLevel = MIN_OXYGEN, - RemoveExistingWhenNecessary = true + RemoveExistingWhenNecessary = true, + TargetSlot = oxygenSourceSlotIndex }; if (container.HasSubContainers) { @@ -167,12 +172,36 @@ namespace Barotrauma } } + private bool IsSuitableContainedOxygenSource(Item item) + { + return + item != null && + item.HasTag(OXYGEN_SOURCE) && + item.Condition > 0 && + (oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value)); + } + + private void TrySetTargetItem(Item item) + { + if (targetItem == item) { return; } + targetItem = item; + if (targetItem != null) + { + oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + } + else + { + oxygenSourceSlotIndex = null; + } + } + public override void Reset() { base.Reset(); getDivingGear = null; getOxygen = null; targetItem = null; + oxygenSourceSlotIndex = null; } public static float GetMinOxygen(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 2eb5b453e..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 && !HumanAIController.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 cf8cf19ae..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) { @@ -413,7 +413,7 @@ namespace Barotrauma } } } - + private void ApplyTreatment(Affliction affliction, Item item) { item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); @@ -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/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index ceeeab73b..b50108c6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -371,7 +371,8 @@ namespace Barotrauma private int CalculateCellCount(int minValue, int maxValue) { if (maxValue == 0) { return 0; } - float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier); + float difficulty = Level.Loaded?.Difficulty ?? 0.0f; + float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier); return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t)); } @@ -381,7 +382,8 @@ namespace Barotrauma float delay = Config.AgentSpawnDelay; float min = delay; float max = delay * 6; - float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor); + float difficulty = Level.Loaded?.Difficulty ?? 0.0f; + float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor); return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index a2e4e7a3a..140aa7c04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -24,7 +24,7 @@ namespace Barotrauma public bool IsAiming => wasAiming; public bool IsAimingMelee => wasAimingMelee; - protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim); + protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim); public float ArmLength => upperArmLength + forearmLength; @@ -278,7 +278,11 @@ namespace Barotrauma // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; - public float LockFlippingUntil; + public float FlipLockTime { get; private set; } + public void LockFlipping(float time = 0.2f) + { + FlipLockTime = (float)Timing.TotalTime + time; + } public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index f0818dd66..4e3da61fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -994,7 +994,7 @@ namespace Barotrauma foreach (Limb l in Limbs) { if (l.IsSevered) { continue; } - if (!l.DoesFlip) { continue; } + if (!l.DoesFlip) { continue; } if (RagdollParams.IsSpritesheetOrientationHorizontal) { //horizontally aligned limbs need to be flipped 180 degrees @@ -1014,7 +1014,7 @@ namespace Barotrauma if (l.IsSevered) { continue; } float rotation = l.body.Rotation; - if (l.DoesFlip) + if (l.DoesMirror) { if (RagdollParams.IsSpritesheetOrientationHorizontal) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b617fef5f..bb4f57c72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -150,8 +150,10 @@ namespace Barotrauma private readonly float movementLerp; - private float cprAnimTimer; - private float cprPump; + private float cprAnimTimer,cprPump; + + private float fallingProneAnimTimer; + const float FallingProneAnimDuration = 1.0f; private bool swimming; //time until the character can switch from walking to swimming or vice versa @@ -268,7 +270,8 @@ namespace Barotrauma if (deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; - UpdateDying(deltaTime); + //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration + UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration); } } else @@ -278,6 +281,11 @@ namespace Barotrauma if (!character.CanMove) { + if (fallingProneAnimTimer < FallingProneAnimDuration) + { + fallingProneAnimTimer += deltaTime; + UpdateFallingProne(1.0f); + } levitatingCollider = false; Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) @@ -291,18 +299,20 @@ namespace Barotrauma } return; } + fallingProneAnimTimer = 0.0f; //re-enable collider if (!Collider.Enabled) { var lowestLimb = FindLowestLimb(); - + Collider.SetTransform(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); + Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity; Collider.Enabled = true; } @@ -431,7 +441,7 @@ namespace Barotrauma } } - if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) + if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck) { Flip(); } @@ -444,7 +454,7 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = false; + IsHanging = IsHanging && character.IsRagdolled; } void UpdateStanding() @@ -1292,10 +1302,9 @@ namespace Barotrauma } } - void UpdateDying(float deltaTime) + void UpdateFallingProne(float strength) { - //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration - float strength = 1.0f - deathAnimTimer / deathAnimDuration; + if (strength <= 0.0f) { return; } Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); @@ -1319,6 +1328,19 @@ namespace Barotrauma } if (torso == null) { return; } + + //make the torso tip over + //otherwise it tends to just drop straight down, pinning the characters legs in a weird pose + if (!InWater) + { + //prefer tipping over in the same direction the torso is rotating + //or moving + //or lastly, in the direction it's facing if it's not moving/rotating + float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f); + float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength; + torso.body.ApplyTorque(torque * torso.body.Mass); + } + //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split for (int i = 0; i < 2; i++) { @@ -1503,12 +1525,12 @@ namespace Barotrauma Limb rightHand = GetLimb(LimbType.RightHand); Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb; + if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb; + if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } if (!target.AllowInput) { @@ -1644,18 +1666,24 @@ namespace Barotrauma pullLimb.PullJointEnabled = true; if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { - Vector2 pullLimbAnchor = targetLimb.SimPosition; pullLimb.PullJointMaxForce = 5000.0f; if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) { targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); } - - Vector2 shoulderPos = rightShoulder.WorldAnchorA; - Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; - if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; } - targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); + Vector2 shoulderPos = rightShoulder.WorldAnchorA; + float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos); + Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist; + if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; } + if (!InWater) + { + //lerp the arm downwards when not swimming + dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f); + } + + Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2); + targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength); targetForce = 200.0f; if (target.Submarine != character.Submarine) { @@ -1723,7 +1751,7 @@ namespace Barotrauma { if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X) { - target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + target.AnimController.LockFlipping(0.5f); } else { @@ -1822,16 +1850,22 @@ namespace Barotrauma public override void Flip() { + if (Character == null || Character.Removed) + { + LogAccessedRemovedCharacterError(); + return; + } + base.Flip(); WalkPos = -WalkPos; Limb torso = GetLimb(LimbType.Torso); - - Vector2 difference; + if (torso == null) { return; } Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation); + Vector2 difference; foreach (Item heldItem in character.HeldItems) { if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent() != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 81e272765..b5e116dab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -57,17 +57,7 @@ namespace Barotrauma { if (limbs == null) { - if (!accessRemovedCharacterErrorShown) - { - string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); - errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "Ragdoll.Limbs:AccessRemoved", - GameAnalyticsManager.ErrorSeverity.Error, - "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); - accessRemovedCharacterErrorShown = true; - } + LogAccessedRemovedCharacterError(); return Array.Empty(); } return limbs; @@ -158,6 +148,20 @@ namespace Barotrauma } } + public bool TryGetCollider(int index, out PhysicsBody collider) + { + collider = null; + try + { + collider = this.collider?[index]; + return true; + } + catch + { + return false; + } + } + public int ColliderIndex { get @@ -873,7 +877,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; } + if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } limb.Dir = Dir; limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); @@ -1428,6 +1432,21 @@ namespace Barotrauma return true; } + protected void LogAccessedRemovedCharacterError() + { + if (!accessRemovedCharacterErrorShown) + { + string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); + errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "Ragdoll:AccessRemoved", + GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); + accessRemovedCharacterErrorShown = true; + } + } + partial void UpdateProjSpecific(float deltaTime, Camera cam); partial void Splash(Limb limb, Hull limbHull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 16657084f..702512ca4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -492,37 +492,52 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); - var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; + var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; + var additionalEffectType = ActionType.OnUse; if (targetCharacter != null && targetCharacter.IsDead) { - effectType = ActionType.OnEating; + additionalEffectType = ActionType.OnEating; } foreach (StatusEffect effect in statusEffects) { effect.sourceBody = sourceBody; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character)) { - // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied - effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition); + var t = sourceLimb ?? attacker as ISerializableEntity; + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition); + } + effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition); } if (effect.HasTargetType(StatusEffect.TargetType.Parent)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, attacker); + } + effect.Apply(additionalEffectType, deltaTime, attacker, attacker); } if (targetCharacter != null) { - if (effect.HasTargetType(StatusEffect.TargetType.Character)) - { - effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter); - } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb); + } + effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast().ToList()); + // TODO: do we need the conversion to list here? It generates garbage. + var targets = targetCharacter.AnimController.Limbs.Cast().ToList(); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets); + } + effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets); } } if (target is Entity targetEntity) @@ -532,18 +547,30 @@ namespace Barotrauma { targets.Clear(); effect.AddNearbyTargets(worldPosition, targets); - effect.Apply(effectType, deltaTime, targetEntity, targets); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets); + } + effect.Apply(additionalEffectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition); + } + effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition); } } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { targets.Clear(); targets.AddRange(attacker.Inventory.AllItems); - effect.Apply(effectType, deltaTime, attacker, targets); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, targets); + } + effect.Apply(additionalEffectType, deltaTime, attacker, targets); } } @@ -579,47 +606,52 @@ namespace Barotrauma } var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); - var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; + var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) { effect.sourceBody = sourceBody; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character)) { - effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); + effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); + effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); } if (effect.HasTargetType(StatusEffect.TargetType.Parent)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + effect.Apply(conditionalEffectType, deltaTime, attacker, attacker); + effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker); } - if (effect.HasTargetType(StatusEffect.TargetType.Character)) + if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character); } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); + // TODO: do we need the conversion to list here? It generates garbage. + var targets = targetLimb.character.AnimController.Limbs.Cast().ToList(); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); effect.AddNearbyTargets(worldPosition, targets); - effect.Apply(effectType, deltaTime, targetLimb.character, targets); - } - if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) - { - effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { targets.Clear(); targets.AddRange(attacker.Inventory.AllItems); - effect.Apply(effectType, deltaTime, attacker, targets); + effect.Apply(conditionalEffectType, deltaTime, attacker, targets); + effect.Apply(ActionType.OnUse, deltaTime, attacker, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 44b96063c..5ee296c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -31,6 +31,9 @@ namespace Barotrauma { public readonly static List CharacterList = new List(); + public const float MaxHighlightDistance = 150.0f; + public const float MaxDragDistance = 200.0f; + partial void UpdateLimbLightSource(Limb limb); private bool enabled = true; @@ -354,9 +357,15 @@ namespace Barotrauma public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + public Identifier Group => Params.Group; + public bool IsHumanoid => Params.Humanoid; + + public bool IsMachine => Params.IsMachine; + public bool IsHusk => Params.Husk; public bool IsMale => info?.IsMale ?? false; @@ -480,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}"); } @@ -743,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); } } @@ -805,9 +814,15 @@ namespace Barotrauma public float HealthPercentage => CharacterHealth.HealthPercentage; public float MaxVitality => CharacterHealth.MaxVitality; public float MaxHealth => MaxVitality; + + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasFullHealth => CharacterHealth.WasInFullHealth; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; + public float PoisonVulnerability => Params.Health.PoisonVulnerability; public float Bloodloss { @@ -1026,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; @@ -1741,7 +1758,7 @@ namespace Barotrauma float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed); targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); - SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier); + SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier)); targetMovement *= SpeedMultiplier; // Reset, status effects will set the value before the next update ResetSpeedMultiplier(); @@ -2201,7 +2218,9 @@ namespace Barotrauma if (SelectedCharacter != null) { - if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected) + if (!SelectedCharacter.CanBeSelected || + (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance && + SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance))) { DeselectCharacter(); } @@ -2494,8 +2513,12 @@ namespace Barotrauma if (!skipDistanceCheck) { - maxDist = ConvertUnits.ToSimUnits(maxDist); - if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } + maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent()); + if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist && + Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist) + { + return false; + } } return checkVisibility ? CanSeeCharacter(c) : true; @@ -2851,6 +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) @@ -3138,56 +3178,57 @@ namespace Barotrauma UpdateAIChatMessages(deltaTime); - //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! - bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; - bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f; - bool wasRagdolled = false; - bool selfRagdolled = false; - - if (IsForceRagdolled) + if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true) { - IsRagdolled = IsForceRagdolled; - } - else if (this != Controlled) - { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); - } - //Keep us ragdolled if we were forced or we're too speedy to unragdoll - else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll)) - { - if (ragdollingLockTimer > 0.0f) + bool wasRagdolled = IsRagdolled; + if (IsForceRagdolled) { - SetInput(InputType.Ragdoll, false, true); - ragdollingLockTimer -= deltaTime; + IsRagdolled = IsForceRagdolled; + } + else if (this != Controlled) + { + wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); } else { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; } + bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); + bool bodyMovingTooFast(PhysicsBody body) + { + return + body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || + //falling down counts as going too fast + (!InWater && body.LinearVelocity.Y < -5.0f); + } + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else if (!tooFastToUnragdoll) + { + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + } + if (IsRagdolled) + { + SetInput(InputType.Ragdoll, false, true); + } } - } - - if (!wasRagdolled && IsRagdolled) - { - if (selfRagdolled) + if (!wasRagdolled && IsRagdolled) { - CheckTalents(AbilityEffectType.OnSelfRagdoll); + CheckTalents(AbilityEffectType.OnRagdoll); } - // currently does not work when you are stunned, like it should - CheckTalents(AbilityEffectType.OnRagdoll); } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); - //ragdoll button if (IsRagdolled || !CanMove) { if (AnimController is HumanoidAnimController humanAnimController) { humanAnimController.Crouching = false; } + if (IsRagdolled) { AnimController.IgnorePlatforms = true; } AnimController.ResetPullJoints(); SelectedItem = SelectedSecondaryItem = null; return; @@ -3365,6 +3406,20 @@ namespace Barotrauma return distSqr; } + public float GetDistanceToClosestLimb(Vector2 simPos) + { + float closestDist = float.MaxValue; + foreach (Limb limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + float dist = Vector2.Distance(simPos, limb.SimPosition); + dist -= limb.body.GetMaxExtent(); + closestDist = Math.Min(closestDist, dist); + if (closestDist <= 0.0f) { return 0.0f; } + } + return closestDist; + } + private float despawnTimer; private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) { @@ -3756,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) @@ -3768,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 @@ -3885,8 +3946,8 @@ namespace Barotrauma { foreach (Affliction affliction in attackResult.Afflictions) { - if (affliction.Strength == 0.0f) continue; - sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}"); + if (Math.Abs(affliction.Strength) <= 0.1f) { continue;} + sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}"); } } GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack); @@ -4087,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); @@ -4108,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)); } /// @@ -4484,7 +4603,10 @@ namespace Barotrauma #if CLIENT //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns - Inventory?.ApplyReceivedState(); + if (GameMain.Client?.ClientPeer is { IsActive: true }) + { + Inventory?.ApplyReceivedState(); + } #endif base.Remove(); @@ -5200,15 +5322,30 @@ namespace Barotrauma public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier); - /// - /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs - /// public bool IsFriendly(Character other) => IsFriendly(this, other); - /// - /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs - /// - public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + 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); + + public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e65fabba5..49c273024 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -778,9 +778,10 @@ namespace Barotrauma FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); - Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White); - Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + //default to transparent color, it's invalid and will be replaced with a random one in CheckColors + Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent); + Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent); + Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent); CheckColors(); TryLoadNameAndTitle(npcIdentifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index b9ff8fe06..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()) { @@ -602,6 +623,18 @@ namespace Barotrauma break; } } + for (int i = 0; i < effects.Count; i++) + { + for (int j = i + 1; j < effects.Count; j++) + { + var a = effects[i]; + var b = effects[j]; + if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength) + { + DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work."); + } + } + } } public void ClearEffects() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 55e681fb8..dcfea21dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -230,6 +230,11 @@ namespace Barotrauma public float StunTimer { get; private set; } + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasInFullHealth { get; private set; } + public Affliction PressureAffliction { get { return pressureAffliction; } @@ -703,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) { @@ -772,6 +777,8 @@ namespace Barotrauma public void Update(float deltaTime) { + WasInFullHealth = vitality >= MaxVitality; + UpdateOxygen(deltaTime); StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; @@ -878,7 +885,11 @@ namespace Barotrauma private void UpdateOxygen(float deltaTime) { - if (!Character.NeedsOxygen) { return; } + if (!Character.NeedsOxygen) + { + oxygenLowAffliction.Strength = 0.0f; + return; + } float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float prevOxygen = OxygenAmount; @@ -980,6 +991,8 @@ namespace Barotrauma UpdateLimbAfflictionOverlays(); UpdateSkinTint(); Character.Kill(type, affliction); + + WasInFullHealth = false; #if CLIENT DisplayVitalityDelay = 0.0f; DisplayedVitality = Vitality; @@ -1024,17 +1037,18 @@ namespace Barotrauma } private readonly List allAfflictions = new List(); - private List GetAllAfflictions(bool mergeSameAfflictions) + private List GetAllAfflictions(bool mergeSameAfflictions, Func predicate = null) { allAfflictions.Clear(); if (!mergeSameAfflictions) { - allAfflictions.AddRange(afflictions.Keys); + allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate)); } else { foreach (Affliction affliction in afflictions.Keys) { + if (predicate != null && !predicate(affliction)) { continue; } var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab); if (existingAffliction == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b79675fa5..79d2eadc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } } - + partial class Limb : ISerializableEntity, ISpatialEntity { //how long it takes for severed limbs to fade out @@ -215,7 +215,7 @@ namespace Barotrauma //the physics body of the limb public PhysicsBody body; - + public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; public Hull Hull; @@ -249,7 +249,7 @@ namespace Barotrauma } } } - + private bool isSevered; private float severedFadeOutTimer; @@ -269,7 +269,7 @@ namespace Barotrauma mouthPos = value; } } - + public readonly Attack attack; public List DamageModifiers { get; private set; } = new List(); @@ -282,39 +282,73 @@ namespace Barotrauma { get { - if (character.AnimController.CurrentAnimationParams is GroundedMovementParams) + if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg) { - switch (type) - { - case LimbType.LeftFoot: - case LimbType.LeftLeg: - case LimbType.LeftThigh: - case LimbType.RightFoot: - case LimbType.RightLeg: - case LimbType.RightThigh: - // Legs always has to flip - return true; - } + // Legs always has to flip when not swimming + return true; } return Params.Flip; } } + public bool DoesMirror + { + get + { + if (IsLeg) + { + // Legs always has to mirror + return true; + } + return DoesFlip; + } + } + public float SteerForce => Params.SteerForce; public Vector2 DebugTargetPos; public Vector2 DebugRefPos; - public bool IsLowerBody => - type == LimbType.LeftLeg || - type == LimbType.RightLeg || - type == LimbType.LeftFoot || - type == LimbType.RightFoot || - type == LimbType.Tail || - type == LimbType.Legs || - type == LimbType.RightThigh || - type == LimbType.LeftThigh || - type == LimbType.Waist; + public bool IsLowerBody + { + get + { + switch (type) + { + case LimbType.LeftLeg: + case LimbType.RightLeg: + case LimbType.LeftFoot: + case LimbType.RightFoot: + case LimbType.Tail: + case LimbType.Legs: + case LimbType.LeftThigh: + case LimbType.RightThigh: + case LimbType.Waist: + return true; + default: + return false; + } + } + } + + public bool IsLeg + { + get + { + switch (type) + { + case LimbType.LeftFoot: + case LimbType.LeftLeg: + case LimbType.LeftThigh: + case LimbType.RightFoot: + case LimbType.RightLeg: + case LimbType.RightThigh: + return true; + default: + return false; + } + } + } public bool IsSevered { @@ -456,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 { @@ -606,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; @@ -738,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) { @@ -757,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/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 950684465..1881758e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -75,13 +75,14 @@ namespace Barotrauma.Abilities if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; } switch (wt) { - // it is possible that an item that has both a melee and a projectile component will return true - // even when not used as a melee/ranged weapon respectively - // attackdata should contain data regarding whether the attack is melee or not case WeaponType.Melee: + //if the item has an active projectile component (has been fired), don't consider it a melee weapon + if (item?.GetComponent() is { IsActive: true }) { continue; } if (item?.GetComponent() != null) { return true; } break; case WeaponType.Ranged: + //if the item has a melee weapon component that's being used now, don't consider it a projectile + if (item?.GetComponent() is { Hitting: true }) { continue; } if (item?.GetComponent() != null) { return true; } break; case WeaponType.HandheldRanged: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 0ac951fd6..c07128f0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -28,7 +28,6 @@ namespace Barotrauma.Abilities conditionals.Add(new PropertyConditional(attribute)); } } - break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 993c19b94..e4580fadd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -37,19 +37,7 @@ namespace Barotrauma.Abilities if (itemPrefab != null) { - if (category != MapEntityCategory.None) - { - if (!itemPrefab.Category.HasFlag(category)) { return false; } - } - - if (identifiers.Any()) - { - if (!identifiers.Any(t => itemPrefab.Identifier == t)) - { - return false; - } - } - return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + return MatchesItem(itemPrefab); } else { @@ -57,5 +45,22 @@ namespace Barotrauma.Abilities return false; } } + + public bool MatchesItem(ItemPrefab itemPrefab) + { + if (category != MapEntityCategory.None) + { + if (!itemPrefab.Category.HasFlag(category)) { return false; } + } + + if (identifiers.Any()) + { + if (!identifiers.Any(t => itemPrefab.Identifier == t)) + { + return false; + } + } + return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 1b4f880f8..6c4022968 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -17,6 +17,14 @@ namespace Barotrauma.Abilities tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); } + public override void InitializeAbility(bool addingFirstTime) + { + if (addingFirstTime) + { + VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f); + } + } + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { if (conditionsMatched) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs index 78d7f501a..60d7abd61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -1,19 +1,35 @@ #nullable enable +using Barotrauma.Extensions; using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Abilities { internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility { - public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + private readonly AbilityConditionItem? condition; + + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem)); + if (conditionElement != null) + { + condition = new AbilityConditionItem(CharacterTalent, conditionElement); + } + } protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } - int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); - ingredients.Items.RemoveAt(randomIndex); + List applicableIngredients = condition == null ? + ingredients.Items.ToList() : + ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList(); + if (applicableIngredients.None()) { return; } + + ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index a53d13328..b4b26579b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -42,7 +42,7 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; public Md5Hash Hash { get; private set; } - public readonly Option InstallTime; + public readonly Option InstallTime; public ImmutableArray Files { get; private set; } @@ -73,7 +73,7 @@ namespace Barotrauma Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); if (item is null) { return true; } - return item.Value.LatestUpdateTime <= installTime; + return item.Value.LatestUpdateTime <= installTime.ToUtcValue(); } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); @@ -106,10 +106,7 @@ namespace Barotrauma GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); - UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); - InstallTime = installTimeUnix != 0 - ? Option.Some(ToolBox.Epoch.ToDateTime(installTimeUnix)) - : Option.None(); + InstallTime = rootElement.GetAttributeDateTime("installtime"); var fileResults = rootElement.Elements() .Select(e => ContentFile.CreateFromXElement(this, e)) @@ -288,9 +285,7 @@ namespace Barotrauma if (errorCatcher.Errors.Any()) { - yield return ContentPackageManager.LoadProgress.Failure( - ContentPackageManager.LoadProgress.Error - .Reason.ConsoleErrorsThrown); + yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text)); yield break; } yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 81056e5eb..88ff41f52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -23,6 +23,8 @@ namespace Barotrauma public const string RegularPackagesElementName = "regularpackages"; public const string RegularPackagesSubElementName = "package"; + public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent); + public static class EnabledPackages { public static CorePackage? Core { get; private set; } = null; @@ -435,22 +437,19 @@ namespace Barotrauma public readonly record struct LoadProgress(Result Result) { public readonly record struct Error( - Error.Reason ErrorReason, - Option Exception) + Either, Exception> ErrorsOrException) { - public enum Reason { Exception, ConsoleErrorsThrown } - - public Error(Reason reason) : this(reason, Option.None) { } - public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { } + public Error(IEnumerable errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { } + public Error(Exception exception) : this(ErrorsOrException: exception) { } } public static LoadProgress Failure(Exception exception) => new LoadProgress( Result.Failure(new Error(exception))); - public static LoadProgress Failure(Error.Reason reason) + public static LoadProgress Failure(IEnumerable errorMessages) => new LoadProgress( - Result.Failure(new Error(reason))); + Result.Failure(new Error(errorMessages))); public static LoadProgress Progress(float value) => new LoadProgress( diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index f2b9dbae6..428050ab2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -88,6 +88,7 @@ namespace Barotrauma public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); + public Range GetAttributeRange(string key, in Range def) => Element.GetAttributeRange(key, def); public Identifier VariantOf() => Element.VariantOf(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 5db14d6ae..a6cb260a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1312,10 +1312,10 @@ namespace Barotrauma } }, () => { - return new[] - { - FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(), - GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + return new[] + { + FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(), + GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() }; }, true)); @@ -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/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 8a9049030..b9695d7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -49,7 +49,6 @@ namespace Barotrauma OnUseRangedWeapon, OnReduceAffliction, OnAddDamageAffliction, - OnSelfRagdoll, OnRagdoll, OnRoundEnd, OnLootCharacter, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index 3182cfae9..ac2beeebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction TurbineOutputSlider, DeconstructButton, RechargeSpeedSlider, - CPRButton + CPRButton, + CloseButton, + MessageBoxCloseButton } [Serialize(ElementId.None, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 7e4da0ea5..ced3da5c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -226,7 +226,7 @@ namespace Barotrauma bool requiresRescue = element.GetAttributeBool("requirerescue", false); - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos); if (spawnPos is WayPoint wp) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 6e46b3c5b..634652ffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -97,7 +97,7 @@ namespace Barotrauma List connectedSubs = level.BeaconStation.GetConnectedSubs(); foreach (Item item in Item.ItemList) { - if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; } if (item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 7b162b544..0ccb6fd13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -404,7 +404,7 @@ namespace Barotrauma { var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); - info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value)); + info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } // apply money gains afterwards to prevent them from affecting XP gains @@ -545,17 +545,14 @@ namespace Barotrauma return humanPrefab; } - protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) + protected static Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient) { var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); characterInfo.TeamID = teamType; - if (positionToStayIn == null) - { - positionToStayIn = + positionToStayIn ??= WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ?? WayPoint.GetRandom(SpawnType.Human, null, submarine); - } Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d8ccd3753..d4e57fc47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -201,7 +201,7 @@ namespace Barotrauma { continue; } - if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2()))) + if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2())) { removals.Add(position); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index a122a213c..fbb52eaea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -371,7 +371,7 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent)) + if (ContentPackageManager.ModsEnabled) { message = "[MODDED] " + message; } 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/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 791d852b4..2377b386c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -82,7 +82,7 @@ namespace Barotrauma } public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; + public const int MaxMissionCountLimit = 3; public const int MinMissionCountLimit = 1; public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index f28cb563c..fa550b175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -46,15 +46,15 @@ namespace Barotrauma public static void Init() { #if CLIENT - Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); - DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); - SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); - TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); + Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true); + DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true); + SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true); + TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true); #endif - Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); - Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); - PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); - MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); + Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false); + Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false); + PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false); + MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/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 569cbac92..5c416862d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -179,14 +179,41 @@ namespace Barotrauma int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); int currentLevel = GetUpgradeLevel(prefab, category); + int newLevel = currentLevel + 1; int maxLevel = prefab.GetMaxLevelForCurrentSub(); if (currentLevel + 1 > maxLevel) { - DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled."); + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled."); return; } + bool TryTakeResources(Character character) + { + bool result = prefab.TryTakeResources(character, newLevel); + if (!result) + { + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources."); + } + return result; + } + + switch (GameMain.NetworkMember) + { + case null when Character.Controlled is { } controlled: // singleplayer + if (!TryTakeResources(controlled)) { return; } + break; + case { IsClient: true }: + if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } + break; + case { IsServer: true } when client?.Character is { } character: + if (!TryTakeResources(character)) { return; } + break; + default: + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); + return; + } + if (price < 0) { Location? location = Campaign.Map?.CurrentLocation; @@ -665,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() { @@ -686,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 40585fad2..3ef615e8a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -188,16 +188,26 @@ namespace Barotrauma.Items.Components private DockingPort FindAdjacentPort() { + float closestDist = float.MaxValue; + DockingPort closestPort = null; foreach (DockingPort port in list) { if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; } - if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) { continue; } - if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) { continue; } + float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X); + if (xDist > DistanceTolerance.X) { continue; } + float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y); + if (yDist > DistanceTolerance.Y) { continue; } - return port; + float dist = xDist + yDist; + //disfavor non-interactable ports + if (port.item.NonInteractable) { dist *= 2; } + if (dist < closestDist) + { + closestPort = port; + closestDist = dist; + } } - - return null; + return closestPort; } private void AttemptDock() @@ -279,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 aa26c2e1d..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,11 @@ 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 > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + + //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; } @@ -459,7 +462,10 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } - linkedGap.Open = 1.0f; + if (linkedGap != null) + { + linkedGap.Open = 1.0f; + } IsOpen = false; #if CLIENT if (convexHull != null) { convexHull.Enabled = 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 e7d089563..548147bfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -49,6 +49,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")] + public bool HitOnlyCharacters + { + get; + set; + } + [Editable, Serialize(true, IsPropertySaveable.No)] public bool Swing { get; set; } @@ -112,7 +119,7 @@ namespace Barotrauma.Items.Components reloadTimer = reload; reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed); reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier); - character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f; + character.AnimController.LockFlipping(); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; @@ -216,10 +223,10 @@ 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.LockFlippingUntil = (float)Timing.TotalTime + Reload; + ac.LockFlipping(); } } else @@ -343,33 +350,36 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure) + else if (!HitOnlyCharacters) { - if (AllowHitMultiple) + if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure) { - if (hitTargets.Contains(targetStructure)) { return true; } + if (AllowHitMultiple) + { + if (hitTargets.Contains(targetStructure)) { return true; } + } + else + { + if (hitTargets.Any(t => t is Structure)) { return true; } + } + hitTargets.Add(targetStructure); } - else + else if (f2.Body.UserData is Item targetItem) { - if (hitTargets.Any(t => t is Structure)) { return true; } + if (AllowHitMultiple) + { + if (hitTargets.Contains(targetItem)) { return true; } + } + else + { + if (hitTargets.Any(t => t is Item)) { return true; } + } + hitTargets.Add(targetItem); } - hitTargets.Add(targetStructure); - } - else if (f2.Body.UserData is Item targetItem) - { - if (AllowHitMultiple) + else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) { - if (hitTargets.Contains(targetItem)) { return true; } + hitTargets.Add(holdable.Item); } - else - { - if (hitTargets.Any(t => t is Item)) { return true; } - } - hitTargets.Add(targetItem); - } - else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) - { - hitTargets.Add(holdable.Item); } else { @@ -381,6 +391,7 @@ namespace Barotrauma.Items.Components return true; } + private System.Text.StringBuilder serverLogger; private void HandleImpact(Fixture targetFixture) { var target = targetFixture.Body; @@ -398,11 +409,13 @@ namespace Barotrauma.Items.Components Character user = User; Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; + Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure; + Item targetItem = target.UserData as Item; + Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; if (Attack != null) { Attack.SetUser(user); Attack.DamageMultiplier = damageMultiplier; - if (targetLimb != null) { if (targetLimb.character.Removed) { return; } @@ -415,12 +428,12 @@ namespace Barotrauma.Items.Components targetCharacter.LastDamageSource = item; Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); } - else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure) + else if (targetStructure != null) { if (targetStructure.Removed) { return; } Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); } - else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) + else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); @@ -457,27 +470,43 @@ namespace Barotrauma.Items.Components { conditionalActionType = ActionType.OnFailure; } - if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null) + if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb)); - #if SERVER - if (GameMain.Server != null) //TODO: Log structure hits + 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}"); + if (item.ContainedItems != null && item.ContainedItems.Any()) { - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})"); } - #endif + string targetName; + if (targetCharacter != null) + { + targetName = targetCharacter.LogName; + } + else if (targetItem != null) + { + targetName = targetItem.Name; + } + else if (targetStructure != null) + { + targetName = targetStructure.Name; + } + else + { + targetName = targetEntity.ToString(); + } + serverLogger.Append($" on {targetName}."); +#if SERVER + Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack); +#endif } - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? + if (targetEntity != null) { - ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 95c40bb0f..9f83723a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components get { return allowedSlots; } } + public bool PickingDone => pickTimer >= PickingTime; + public Character Picker { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 0ac7dcac2..774bf603b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -213,6 +213,8 @@ namespace Barotrauma.Items.Components baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); } ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier); + currentChargeTime = 0f; if (character != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index bf808c48a..1451570d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; @@ -185,24 +184,23 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character); + bool failed = false; if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } - if (UsableIn == UseEnvironment.None) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } - if (item.InWater) { if (UsableIn == UseEnvironment.Air) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } } else @@ -210,9 +208,15 @@ namespace Barotrauma.Items.Components if (UsableIn == UseEnvironment.Water) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } } + if (failed) + { + // Always apply ActionType.OnUse. If doesn't fail, the effect is called later. + ApplyStatusEffects(ActionType.OnUse, deltaTime, character); + return false; + } Vector2 rayStart; Vector2 rayStartWorld; @@ -312,9 +316,12 @@ namespace Barotrauma.Items.Components var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them - if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse)) + if (statusEffectLists != null) { - if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f)) + static bool CanSeverJoints(ActionType type, Dictionary> effectList) => + effectList.TryGetValue(type, out List effects) && effects.Any(e => e.SeverLimbsProbability > 0); + + if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists)) { float rangeSqr = ConvertUnits.ToSimUnits(Range); rangeSqr *= rangeSqr; @@ -537,6 +544,7 @@ namespace Barotrauma.Items.Components if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); float structureFixAmount = StructureFixAmount; @@ -605,6 +613,7 @@ namespace Barotrauma.Items.Components } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } @@ -621,6 +630,7 @@ namespace Barotrauma.Items.Components targetLimb.character.LastDamageSource = item; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } @@ -663,6 +673,7 @@ namespace Barotrauma.Items.Components targetItem.IsHighlighted = true; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem); if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { @@ -875,7 +886,7 @@ namespace Barotrauma.Items.Components } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) { - currentTargets.Add(character); + currentTargets.Add(user); effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index edf91b18e..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, 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 01d0dcc71..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)); @@ -856,7 +853,13 @@ namespace Barotrauma.Items.Components if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } effect.AfflictionMultiplier = afflictionMultiplier; - item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); + var c = character; + if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + // A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user. + c = user; + } + item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } 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/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 0499ce20d..b1a12a52c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components //disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll if (character.AnimController is HumanoidAnimController humanoidAnim) { - humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + humanoidAnim.LockFlipping(0.5f); } if (character.SelectedItem == item) { character.SelectedItem = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index aecb9a670..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); } } @@ -767,7 +774,7 @@ namespace Barotrauma.Items.Components limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition); return false; } - if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User)) + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) { return false; } @@ -888,7 +895,7 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { - if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User)) + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) { return false; } @@ -959,8 +966,8 @@ namespace Barotrauma.Items.Components { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User); + ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User); var attack = targetLimb.attack; if (attack != null) { @@ -971,22 +978,30 @@ namespace Barotrauma.Items.Components { if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); + effect.Apply(effect.type, 1.0f, User, User); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character); + } + if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + { + effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); effect.AddNearbyTargets(targetLimb.WorldPosition, targets); - effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); + effect.Apply(effect.type, 1.0f, targetLimb.character, targets); } } } } 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 @@ -995,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)); } } } @@ -1004,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; } @@ -1020,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/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 75426b030..a41e1e1c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -83,7 +83,15 @@ namespace Barotrauma.Items.Components { if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; } - float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f); + float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, level.LevelData.Biome.ActualMaxDifficulty / 100.0f); + + if (level.Type == LevelData.LevelType.Outpost && + level.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + //no high-quality spawns in friendly outposts + difficultyFactor = 0.0f; + } + return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync); static float GetCommonness(int quality, float difficultyFactor) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 7f0c5ca8e..d15979d30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -159,9 +159,11 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + var user = item.GetComponent()?.User; if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed) || - (source is Limb limb && limb.Removed)) + (source is Limb limb && limb.Removed) || + (user != null && user.Removed)) { ResetSource(); target = null; @@ -293,7 +295,6 @@ namespace Barotrauma.Items.Components { targetMass = float.MaxValue; } - var user = item.GetComponent()?.User; if (targetMass > TargetMinMass) { if (Math.Abs(SourcePullForce) > 0.001f) @@ -301,33 +302,16 @@ namespace Barotrauma.Items.Components var sourceBody = GetBodyToPull(source); if (sourceBody != null) { - var targetBody = GetBodyToPull(target); - if (targetBody != null && !(targetBody.UserData is 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; @@ -340,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 1ccd035f9..da887ef93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value, value ? lightBrightness : 0.0f); + SetLightSourceState(value); OnStateChanged(); } } @@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsOn ? lightColor.Multiply(currentBrightness) : Color.Transparent; + Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent; } #endif } @@ -187,6 +187,15 @@ 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) { #if CLIENT @@ -205,7 +214,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value, value ? lightBrightness : 0.0f); + SetLightSourceState(value); } } @@ -236,9 +245,10 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - SetLightSourceState(IsActive, lightBrightness); + SetLightSourceState(IsActive); turret = item.GetComponent(); #if CLIENT + Drawable = AlphaBlend && Light.LightSprite != null; if (Screen.Selected.IsEditor) { OnMapLoaded(); @@ -263,8 +273,7 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - lightBrightness = 1.0f; - SetLightSourceState(true, lightBrightness); + SetLightSourceState(true); SetLightSourceTransformProjSpecific(); base.IsActive = false; isOn = true; @@ -285,13 +294,15 @@ namespace Barotrauma.Items.Components UpdateAITarget(item.AiTarget); } UpdateOnActiveEffects(deltaTime); + //something in UpdateOnActiveEffects may deactivate the light -> return so we don't turn it back on + if (!IsActive) { return; } #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null && !(item.GetRootInventoryOwner() is Character)) + if (item.Container != null && item.GetRootInventoryOwner() is not Character) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); return; } @@ -300,12 +311,14 @@ namespace Barotrauma.Items.Components PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); return; } + TemporaryFlickerTimer -= deltaTime; + //currPowerConsumption = powerConsumption; - if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) + if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f)) { #if CLIENT if (Voltage > 0.1f) @@ -325,7 +338,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); } public override bool Use(float deltaTime, Character character = null) @@ -357,7 +370,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled, currentBrightness); + SetLightSourceState(Light.Enabled); #endif prevColorSignal = signal.value; } @@ -375,7 +388,7 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } - partial void SetLightSourceState(bool enabled, float brightness); + partial void SetLightSourceState(bool enabled, float? brightness = null); public void SetLightSourceTransform() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 1240fb9cb..f596fd3d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components private string prevSignal; - private readonly int[] channelMemory = new int[ChannelMemorySize]; + private int[] channelMemory = new int[ChannelMemorySize]; private Connection signalInConnection; private Connection signalOutConnection; @@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components { list.Add(this); IsActive = true; - channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + if (channelMemory.Length != ChannelMemorySize) + { + DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing..."); + Array.Resize(ref channelMemory, ChannelMemorySize); + } } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 863210c6b..09dfe9080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { - partial class WireSection + public partial class WireSection { private Vector2 start; private Vector2 end; @@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public static IEnumerable ExtractNodes(XElement element) { - base.Load(componentElement, usePrefabValues, idRemap); - - string nodeString = componentElement.GetAttributeString("nodes", ""); - if (nodeString == "") return; + string nodeString = element.GetAttributeString("nodes", ""); + if (nodeString.IsNullOrWhiteSpace()) { yield break; } string[] nodeCoords = nodeString.Split(';'); for (int i = 0; i < nodeCoords.Length / 2; i++) { - float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x); - float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y); - nodes.Add(new Vector2(x, y)); + float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x); + float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y); + yield return new Vector2(x, y); } + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + + nodes.AddRange(ExtractNodes(componentElement)); Drawable = nodes.Any(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 3b98d93bc..221ce222a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -445,7 +445,9 @@ namespace Barotrauma.Items.Components { // single charged shot guns will decharge after firing // for cosmetic reasons, this is done by lerping in half the reload time - currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)); + currentChargeTime = reloadTime > 0.0f ? + Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) : + 0.0f; } else { 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 20e17a56d..c31f6c337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -268,6 +268,8 @@ namespace Barotrauma get { return capacity; } } + public int EmptySlotCount => slots.Count(i => !i.Empty()); + public bool AllowSwappingContainedItems = true; public Inventory(Entity owner, int capacity, int slotsPerRow = 5) @@ -583,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 @@ -887,10 +891,7 @@ namespace Barotrauma } if (recursive) { - if (item.OwnInventory != null) - { - item.OwnInventory.FindAllItems(predicate, recursive: true, list); - } + item.OwnInventory?.FindAllItems(predicate, recursive: true, list); } } return list; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c5d74c0d9..346f772dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -111,6 +111,7 @@ namespace Barotrauma private float sendConditionUpdateTimer; private bool conditionUpdatePending; + private float prevCondition; private float condition; private bool inWater; @@ -896,7 +897,7 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition = Prefab.Health; + condition = MaxCondition = prevCondition = Prefab.Health; ConditionPercentage = 100.0f; lastSentCondition = condition; @@ -998,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; } } @@ -1019,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) { @@ -1581,7 +1583,7 @@ namespace Barotrauma tags.Add(newTag); } - public IEnumerable GetTags() + public IReadOnlyCollection GetTags() { return tags; } @@ -1743,15 +1745,15 @@ namespace Barotrauma if (Indestructible) { return; } if (InvulnerableToDamage && value <= condition) { return; } - float prev = condition; bool wasInFullCondition = IsFullCondition; condition = MathHelper.Clamp(value, 0.0f, MaxCondition); - if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; } + if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; } RecalculateConditionValues(); - if (condition == 0.0f && prev > 0.0f) + bool wasPreviousConditionChanged = false; + if (condition == 0.0f && prevCondition > 0.0f) { //Flag connections to be updated as device is broken flagChangedConnections(connections); @@ -1763,9 +1765,11 @@ 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 && prev <= 0.0f) + else if (condition > 0.0f && prevCondition <= 0.0f) { //Flag connections to be updated as device is now working again flagChangedConnections(connections); @@ -1793,8 +1797,18 @@ namespace Barotrauma } } - LastConditionChange = condition - prev; - ConditionLastUpdated = Timing.TotalTime; + if (!wasPreviousConditionChanged) + { + SetPreviousCondition(); + } + + void SetPreviousCondition() + { + LastConditionChange = condition - prevCondition; + ConditionLastUpdated = Timing.TotalTime; + prevCondition = condition; + wasPreviousConditionChanged = true; + } static void flagChangedConnections(Dictionary connections) { @@ -2159,8 +2173,9 @@ namespace Barotrauma var projectile = GetComponent(); if (projectile != null) { - //ignore character colliders (a projectile only hits limbs) - if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; } + // Ignore characters so that the impact sound only plays when the item hits a a wall or a door. + // Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this. + if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; } if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; } if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } @@ -2694,7 +2709,7 @@ namespace Barotrauma return; } - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2711,7 +2726,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2725,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; @@ -2742,7 +2757,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); + ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2761,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)); @@ -2781,13 +2803,13 @@ namespace Barotrauma #endif ic.WasUsed = true; - ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user); - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, 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; } @@ -3448,7 +3470,7 @@ namespace Barotrauma item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); } } - item.lastSentCondition = item.condition; + item.lastSentCondition = item.prevCondition = item.condition; item.RecalculateConditionValues(); item.SetActiveSprite(); @@ -3522,15 +3544,10 @@ namespace Barotrauma upgrade.Save(element); } - if (condition < MaxCondition) - { - element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); - } - else - { - var conditionAttribute = element.GetAttribute("condition"); - if (conditionAttribute != null) { conditionAttribute.Remove(); } - } + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); + + var conditionAttribute = element.GetAttribute("condition"); + if (conditionAttribute != null) { conditionAttribute.Remove(); } parentElement.Add(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 77b03826f..4ad6b2731 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -30,7 +30,7 @@ namespace Barotrauma public EventType EventType { get; } } - public struct ComponentStateEventData : IEventData + public readonly struct ComponentStateEventData : IEventData { public EventType EventType => EventType.ComponentState; public readonly ItemComponent Component; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 83d4a3788..761e84d24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -568,9 +568,11 @@ namespace Barotrauma public ImmutableDictionary LevelQuantity { get; private set; } - public bool CanSpriteFlipX { get; private set; } + private bool canSpriteFlipX; + public override bool CanSpriteFlipX => canSpriteFlipX; - public bool CanSpriteFlipY { get; private set; } + private bool canSpriteFlipY; + public override bool CanSpriteFlipY => canSpriteFlipY; /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. @@ -767,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); @@ -884,8 +889,8 @@ namespace Barotrauma case "sprite": string spriteFolder = GetTexturePath(subElement, variantOf); - CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); - CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); + canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); + canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); if (subElement.GetAttribute("sourcerect") == null && @@ -936,14 +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/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index c05c9517a..b22a9280e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -188,6 +188,12 @@ namespace Barotrauma item.Condition -= item.MaxCondition * EmpStrength * distFactor; } + var lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f); + } + //discharge batteries var powerContainer = item.GetComponent(); if (powerContainer != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index ab5c17c8b..f37d520d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -50,6 +50,12 @@ namespace Barotrauma Description = TextManager.Get($"EntityDescription.{Identifier}"); Tags = Enumerable.Empty().ToImmutableHashSet(); + string description = element.GetAttributeString("description", string.Empty); + if (!description.IsNullOrEmpty()) + { + Description = Description.Fallback(description); + } + List containedItemIDs = new List(); foreach (XElement entityElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index eec99f20b..72bbace41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -860,6 +860,7 @@ namespace Barotrauma (endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) || (endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; } if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; } + if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; } potentialIslands.Add(cell); } for (int i = 0; i < GenerationParams.IslandCount; i++) @@ -1099,6 +1100,7 @@ namespace Barotrauma caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); foreach (var caveCell in caveCells) { + if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; } if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) { var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); @@ -3176,7 +3178,7 @@ namespace Barotrauma TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); - if (!cells.Any(c => c.IsPointInside(startPos + offset))) + if (!IsPositionInsideWall(startPos + offset)) { startPos += offset; } @@ -3232,10 +3234,9 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - //avoid floating ice chunks on the main path if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) { - suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2())))); + suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } if (!suitablePositions.Any()) { @@ -3288,10 +3289,16 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position; return true; } + public bool IsPositionInsideWall(Vector2 worldPosition) + { + var closestCell = GetClosestCell(worldPosition); + return closestCell != null && closestCell.IsPointInside(worldPosition); + } + public void Update(float deltaTime, Camera cam) { LevelObjectManager.Update(deltaTime); @@ -3334,14 +3341,13 @@ namespace Barotrauma public Vector2 GetBottomPosition(float xPosition) { - int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1)); + float interval = Size.X / (bottomPositions.Count - 1); + + int index = (int)Math.Floor(xPosition / interval); if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } - float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); - //t can go slightly outside the 0-1 due to rounding, safe to ignore - Debug.Assert(t <= 1.001f && t >= -0.001f); + float t = (xPosition - bottomPositions[index].X) / interval; t = MathHelper.Clamp(t, 0.0f, 1.0f); - float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); return new Vector2(xPosition, yPos); 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/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 0d1956a5e..df2c0679c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -171,7 +171,7 @@ namespace Barotrauma } int startLocationindex = element.GetAttributeInt("startlocation", -1); - if (startLocationindex > 0 && startLocationindex < Locations.Count) + if (startLocationindex >= 0 && startLocationindex < Locations.Count) { StartLocation = Locations[startLocationindex]; } @@ -188,7 +188,7 @@ namespace Barotrauma } } int endLocationindex = element.GetAttributeInt("endlocation", -1); - if (endLocationindex > 0 && endLocationindex < Locations.Count) + if (endLocationindex >= 0 && endLocationindex < Locations.Count) { EndLocation = Locations[endLocationindex]; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 6d9077cc0..2c9624004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -174,6 +174,9 @@ namespace Barotrauma public abstract Sprite Sprite { get; } + public virtual bool CanSpriteFlipX { get; } = false; + public virtual bool CanSpriteFlipY { get; } = false; + public abstract string OriginalName { get; } public abstract LocalizedString Name { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 9c76e0bb2..414c4c8da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -124,9 +124,18 @@ namespace Barotrauma int eventCount = GameMain.Server.EntityEventManager.Events.Count(); int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count(); #endif - List entities = MapEntity.mapEntityList.FindAll(e => e.Submarine == sub); + HashSet connectedSubs = new HashSet() { sub }; + foreach (Submarine otherSub in Submarine.Loaded) + { + //remove linked subs too + if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); } + } + List entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); entities.ForEach(e => e.Remove()); - sub.Remove(); + foreach (Submarine otherSub in connectedSubs) + { + otherSub.Remove(); + } #if SERVER //remove any events created during the removal of the entities GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 3dd5eea35..18e48a4c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -20,8 +20,8 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; - public readonly bool CanSpriteFlipX; - public readonly bool CanSpriteFlipY; + public override bool CanSpriteFlipX { get; } + public override bool CanSpriteFlipY { get; } /// /// If null, the orientation is determined automatically based on the dimensions of the structure instances diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index e2958efdf..d5f4e7387 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1310,7 +1310,7 @@ namespace Barotrauma return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); } - public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) + public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { upgradeEventIdentifier = new Identifier($"Submarine{ID}"); Loading = true; @@ -1381,65 +1381,65 @@ namespace Barotrauma center.Y -= center.Y % GridSize.Y; RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + } - subBody = new SubmarineBody(this, showWarningMessages); - Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); - subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); + subBody = new SubmarineBody(this, showErrorMessages); + Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); + subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); - if (info.IsOutpost) + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = CharacterTeamType.FriendlyNPC; + + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + + foreach (MapEntity me in MapEntity.mapEntityList) { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = CharacterTeamType.FriendlyNPC; - - bool indestructible = - GameMain.NetworkMember != null && - !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && - !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); - - foreach (MapEntity me in MapEntity.mapEntityList) + if (me.Submarine != this) { continue; } + if (me is Item item) { - if (me.Submarine != this) { continue; } - if (me is Item item) + item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; + item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + if (item.GetComponent() != null && indestructible) { - item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; - item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; - if (item.GetComponent() != null && indestructible) + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) { - item.Indestructible = true; - } - foreach (ItemComponent ic in item.Components) - { - if (ic is ConnectionPanel connectionPanel) + //prevent rewiring + if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) { - //prevent rewiring - if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) - { - connectionPanel.Locked = true; - } + connectionPanel.Locked = true; } - else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) - { - //prevent deattaching items from walls + } + else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) + { + //prevent deattaching items from walls #if CLIENT if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } #endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } + holdable.CanBePicked = false; + holdable.CanBeSelected = false; } } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) - { - structure.Indestructible = true; - } + } + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) + { + structure.Indestructible = true; } } - else if (info.IsRuin) - { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - } + } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; } if (entityGrid != null) @@ -1481,7 +1481,7 @@ namespace Barotrauma #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && + if (showErrorMessages && !string.IsNullOrEmpty(Info.FilePath) && Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index fa7470faa..7867f1166 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -109,7 +109,7 @@ namespace Barotrauma get { return submarine; } } - public SubmarineBody(Submarine sub, bool showWarningMessages = true) + public SubmarineBody(Submarine sub, bool showErrorMessages = true) { this.submarine = sub; @@ -119,9 +119,9 @@ namespace Barotrauma if (!Hull.HullList.Any(h => h.Submarine == sub)) { farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f); - if (showWarningMessages) + if (showErrorMessages) { - DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed."); + DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed."); } } else 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/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 93a7b09eb..d453464f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Networking public T GetVote(VoteType voteType) { - return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T); + return (votes[(int)voteType] is T t) ? t : default; } public void SetVote(VoteType voteType, object value) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs index 1547b1b5a..cf944004e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs @@ -34,7 +34,7 @@ interface IServerPositionSync : IServerSerializable { #if SERVER - void ServerWritePosition(IWriteMessage msg, Client c); + void ServerWritePosition(ReadWriteMessage tempBuffer, Client c); #endif #if CLIENT void ClientReadPosition(IReadMessage msg, float sendingTime); 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/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index ac4b80df1..a1ce193dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -99,6 +99,19 @@ namespace Barotrauma.Networking EntityEventInitial } + [NetworkSerialize] + readonly record struct EntityPositionHeader( + bool IsItem, + UInt32 PrefabUintIdentifier, + UInt16 EntityId) : INetSerializableStruct + { + public static EntityPositionHeader FromEntity(Entity entity) + => new ( + IsItem: entity is Item, + PrefabUintIdentifier: entity is MapEntity me ? me.Prefab.UintIdentifier : 0, + EntityId: entity.ID); + } + enum TraitorMessageType { Server, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index bcc5c90b9..c8042a945 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -171,6 +171,8 @@ namespace Barotrauma.Networking public bool ShouldCreateAnalyticsEvent => DisconnectReason is not ( DisconnectReason.Disconnected + or DisconnectReason.ServerShutdown + or DisconnectReason.ServerFull or DisconnectReason.Banned or DisconnectReason.Kicked or DisconnectReason.TooManyFailedLogins @@ -300,7 +302,7 @@ namespace Barotrauma.Networking public ServerContentPackage() { } - public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime) + public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime) { Name = contentPackage.Name; Hash = contentPackage.Hash; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index e4cbc1bb7..01cbd8d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -1093,7 +1093,7 @@ namespace Barotrauma.Networking for (int i = 0; i < count; i++) { int index = msg.ReadUInt16(); - if (index < 0 || index >= subList.Count) { continue; } + if (index >= subList.Count) { continue; } string submarineName = subList[index].Name; HiddenSubs.Add(submarineName); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 594171239..14ae70d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; - private IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) + private static IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) { Dictionary voteList = new Dictionary(); @@ -29,7 +29,7 @@ namespace Barotrauma return voteList; } - public T HighestVoted(VoteType voteType, List voters) + public static T HighestVoted(VoteType voteType, IEnumerable voters) { if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 09e78f943..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) { @@ -621,6 +643,15 @@ namespace Barotrauma return stringValue.Split(';').Select(s => ParseTuple(s, default)).ToArray(); } + public static Range GetAttributeRange(this XElement element, string name, Range defaultValue) + { + var attribute = element?.GetAttribute(name); + if (attribute is null) { return defaultValue; } + + string stringValue = attribute.Value; + return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue); + } + public static string ElementInnerText(this XElement el) { StringBuilder str = new StringBuilder(); @@ -895,6 +926,37 @@ namespace Barotrauma return floatArray; } + // parse a range string, e.g "1-3" or "3" + public static Range ParseRange(string rangeString) + { + if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); } + + string[] split = rangeString.Split('-'); + return split.Length switch + { + 1 when TryParseInt(split[0], out int value) => new Range(value, value), + 2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range(min, max), + _ => GetDefault(rangeString) + }; + + static bool TryParseInt(string value, out int result) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result); + } + + result = default; + return false; + } + + static Range GetDefault(string rangeString) + { + DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)"); + return new Range(0, 99); + } + } + public static Identifier VariantOf(this XElement element) => element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", "")); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index b327c3903..5776bc9e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -64,7 +64,6 @@ namespace Barotrauma EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, - EditorDisclaimerShown = false, ShowOffensiveServerPrompt = true, TutorialSkipWarning = true, CorpseDespawnDelay = 600, @@ -132,7 +131,6 @@ namespace Barotrauma public EnemyHealthBarMode ShowEnemyHealthBars; public bool ChatOpen; public bool CrewMenuOpen; - public bool EditorDisclaimerShown; public bool ShowOffensiveServerPrompt; public bool TutorialSkipWarning; public int CorpseDespawnDelay; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 301137e0a..b241816f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -75,6 +75,7 @@ namespace Barotrauma case "targetgrandparent": case "targetcontaineditem": case "skillrequirement": + case "targetslot": return false; default: return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 2d9b3723c..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 + } + } } } @@ -1760,12 +1761,17 @@ namespace Barotrauma void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) { + Item parentItem = entity as Item; + if (user == null && parentItem != null) + { + // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) + SetUser(parentItem.GetComponent()?.User); + } switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { - Item parentItem = entity as Item; Projectile projectile = newItem.GetComponent(); if (entity != null) { @@ -2068,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); } } @@ -2106,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 + } + } } } } @@ -2165,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); } @@ -2178,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 e04405ea5..78cd9451d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -47,7 +47,7 @@ namespace Barotrauma.Steam int prevSize = 0; for (int i = 1; i <= (maxPages ?? int.MaxValue); i++) { - Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); + using Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); if (page is not { Entries: var entries }) { break; } // This queries the results on the i-th page and stores them, @@ -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 84b97b6b0..f7b33b9aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -257,10 +257,59 @@ namespace Barotrauma } } - internal partial class UpgradePrefab : UpgradeContentPrefab + internal readonly struct UpgradeResourceCost + { + public readonly int Amount; + private readonly ImmutableArray targetTags; + public readonly Range TargetLevels; + + public UpgradeResourceCost(ContentXElement element) + { + Amount = element.GetAttributeInt("amount", 0); + targetTags = element.GetAttributeIdentifierArray("item", Array.Empty())!.ToImmutableArray(); + TargetLevels = element.GetAttributeRange("levels", new Range(0, 99)); + } + + public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel); + + public bool AppliesForLevel(Range newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start; + + public bool MatchesItem(Item item) => MatchesItem(item.Prefab); + + public bool MatchesItem(ItemPrefab item) + { + foreach (Identifier tag in targetTags) + { + if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; } + } + + return false; + } + } + + internal readonly struct ApplicableResourceCollection + { + public readonly ImmutableArray MatchingItems; + public readonly UpgradeResourceCost Cost; + public readonly int Count; + + public ApplicableResourceCollection(IEnumerable matchingItems, int count, UpgradeResourceCost cost) + { + MatchingItems = matchingItems.ToImmutableArray(); + Count = count; + Cost = cost; + } + + public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost) + { + return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost); + } + } + + internal sealed partial class UpgradePrefab : UpgradeContentPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection( - onAdd: (prefab, isOverride) => + onAdd: static (prefab, isOverride) => { if (!prefab.SuppressWarnings && !isOverride) { @@ -329,6 +378,7 @@ namespace Barotrauma private Dictionary targetProperties { get; } private readonly ImmutableArray MaxLevelsMods; + public readonly ImmutableHashSet ResourceCosts; public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { @@ -341,6 +391,7 @@ namespace Barotrauma var targetProperties = new Dictionary(); var maxLevels = new List(); + var resourceCosts = new HashSet(); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); if (!nameIdentifier.IsEmpty) @@ -383,6 +434,11 @@ namespace Barotrauma maxLevels.Add(new UpgradeMaxLevelMod(subElement)); break; } + case "resourcecost": + { + resourceCosts.Add(new UpgradeResourceCost(subElement)); + break; + } #if CLIENT case "decorativesprite": { @@ -414,6 +470,7 @@ namespace Barotrauma this.targetProperties = targetProperties; MaxLevelsMods = maxLevels.ToImmutableArray(); + ResourceCosts = resourceCosts.ToImmutableHashSet(); upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? .ToImmutableHashSet() ?? ImmutableHashSet.Empty; @@ -456,6 +513,58 @@ namespace Barotrauma return GetMaxLevel(info) > 0; } + public bool HasResourcesToUpgrade(Character? character, int currentLevel) + { + if (character is null) { return false; } + if (!ResourceCosts.Any()) { return true; } + + List allItems = character.Inventory.FindAllItems(recursive: true); + return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem)); + } + + public bool TryTakeResources(Character character, int currentLevel) + { + IEnumerable costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); + + if (!costs.Any()) { return true; } + + List allItems = character.Inventory.FindAllItems(recursive: true); + HashSet itemsToRemove = new HashSet(); + + foreach (UpgradeResourceCost cost in costs) + { + int amountNeeded = cost.Amount; + foreach (Item item in allItems.Where(cost.MatchesItem)) + { + itemsToRemove.Add(item); + amountNeeded--; + if (amountNeeded <= 0) { break; } + } + + if (amountNeeded > 0) { return false; } + } + + foreach (Item item in itemsToRemove) + { + Entity.Spawner.AddItemToRemoveQueue(item); + } + + if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); } + + return true; + } + + public ImmutableArray GetApplicableResources(int level) + { + var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet(); + + var costs = applicableCosts.Any() + ? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray() + : ImmutableArray.Empty; + + return costs; + } + public bool IsDisallowed(MapEntity item) { return item.DisallowedUpgradeSet.Contains(Identifier) 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/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index 9fdc1e786..c4ce599c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -212,8 +212,13 @@ namespace Barotrauma return false; } + public override int GetHashCode() + { + return ShortRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + public static bool operator ==(Md5Hash? a, Md5Hash? b) - => (a is null == b is null) && (a?.Equals(b) ?? true); + => Equals(a, b); public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs index eaf89a4f1..9a5393a32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - public bool Contains(in T v) + public readonly bool Contains(in T v) => start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0; private void VerifyStartLessThanEnd() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 55120383d..f4adb8a06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using System.Text.RegularExpressions; using Barotrauma.IO; using Microsoft.Xna.Framework; +using System.Collections.Immutable; namespace Barotrauma { @@ -292,10 +293,11 @@ namespace Barotrauma } if (doc?.Root == null) { - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: Option.None, + SubmarineName: "", + EnabledContentPackageNames: ImmutableArray.Empty)); } else { @@ -326,13 +328,11 @@ namespace Barotrauma enabledContentPackageNames.Add(packageName.Replace(@"\|", "|")); } - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file, - SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), - SaveTime = doc.Root.GetAttributeInt("savetime", 0), - EnabledContentPackageNames = enabledContentPackageNames.ToArray(), - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: doc.Root.GetAttributeDateTime("savetime"), + SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), + EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray())); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs index 63f652f57..556ca0fd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Networking; */ [NetworkSerialize] -public readonly record struct Segment(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct; +public readonly record struct Segment(T Identifier, int Pointer) : INetSerializableStruct where T : struct; readonly ref struct SegmentTableWriter where T : struct { @@ -94,7 +94,7 @@ readonly ref struct SegmentTableWriter where T : struct public static SegmentTableWriter StartWriting(IWriteMessage msg) { var retVal = new SegmentTableWriter(msg, msg.BitPosition); - msg.WriteUInt16(0); //reserve space for the table pointer + msg.WriteInt32(0); //reserve space for the table pointer return retVal; } @@ -104,28 +104,22 @@ readonly ref struct SegmentTableWriter where T : struct { throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>"); } - - if (message.BitPosition - PointerLocation > UInt16.MaxValue) - { - throw new OverflowException( - $"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)"); - } } - + public void StartNewSegment(T value) { ThrowOnInvalidState(); - segments.Add(new Segment(value, (UInt16)(message.BitPosition-PointerLocation))); + segments.Add(new Segment(value, message.BitPosition - PointerLocation)); } public void Dispose() { ThrowOnInvalidState(); int tablePosition = message.BitPosition; - + //rewrite the table pointer now that we know where the table ends message.BitPosition = PointerLocation; - message.WriteUInt16((UInt16)(tablePosition-PointerLocation)); + message.WriteInt32(tablePosition - PointerLocation); //write the table message.BitPosition = tablePosition; @@ -274,7 +268,7 @@ readonly ref struct SegmentTableReader where T : struct ExceptionHandler? exceptionHandler = null) { int pointerLocation = msg.BitPosition; - int tablePointer = msg.ReadUInt16(); + int tablePointer = msg.ReadInt32(); int tableLocation = pointerLocation + tablePointer; int returnPosition = msg.BitPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs new file mode 100644 index 000000000..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 51a641e51..8f63361d2 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,169 @@ +--------------------------------------------------------------------------------------------------------- +v0.21.6.0 +--------------------------------------------------------------------------------------------------------- + +- Minor localization fixes. +- Fixed some occasional crashes in the character editor. + +--------------------------------------------------------------------------------------------------------- +v0.21.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed delete save button not working in the singleplayer "load game" menu. +- Fixed depleted fuel revolver round recipe requiring too many materials. +- Reduced medical item cooldown to 0.5 seconds. +- Minor fixes to Chinese localization. +- Fix bugged sufforing poisoning when it reaches the full strength. +- Fixed crashing when you save a sub that contains item variants with decorative sprites (most wrecks) as a new content package. + +--------------------------------------------------------------------------------------------------------- +v0.21.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when an item spawns after upgrades with material requirements have been purchased in the multiplayer campaign. +- Fixed crashing if the submarine preview is closed before generating the preview finishes. + +Changes: +- Some submarine upgrades cost materials in addition to money. +- Crush depths are displayed on the submarine switch terminal. +- Miscellaneous fixes to all translations. +- The "max missions" campaign setting is restricted to a maximum of 3. +- Minor improvements to ragdoll animation when falling prone due to stun/unconsciousness/ragdolling. +- Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills. +- Changed Alarm Buzzer sound to differentiate it from the diving suit warning beep. +- Depth charge tweaks: require UEX, increase non-contained item explosion radius and damage, allow quality to affect explosion, increase pricing. +- Reduced Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. +- Lights flicker when hit by EMP. +- Large monsters no longer drop the loot when they die. This was implemented as a workaround to it being difficult to grab large monsters, but now they're much easier to grab - you can grab them anywhere near their main limb, instead of having to find the "origin" somewhere at the center of the monster. +- Bots now prefer PUCS over the other diving suits and the other diving suits over exosuit. +- Made drinks and other consumables work more consistently. +- Adjusted the aim poses used while holding certain melee weapons. +- Harpoons: Adjusted the damage on walls and the projectile launch forces. Spears can now stick to the walls. Inversed the reeling logic: ragdoll key = reel in. + +Poison overhaul: +- Reworked most poison afflictions with the goal that a medic can now use them offensively on monsters and humans alike. +- Sufforin, Morbusine, Cyanide, Paralyzant now work on most monsters. Bigger monsters are generally more resistant than the smaller. +- Sufforin poisoning: Slowly makes the target fall sick. Eventually leads to death. Stronger monsters may recover. +- Morbusine poisoning: Kills the target relatively quickly from gradual oxygen deprivation/organ damage. Stronger monsters may recover. +- Cyanide poisoning: Doesn't do much at first, but progresses rapidly and kills the target suddenly. Lethal even to the strongest monsters. +- Paralysis: Advances more quickly than previously. Effective even on larger monsters. (Leucocytes don't always apply enough paralysis for it to progress. They also use a version of the affliction that progresses using the old, slower speed.) +- Poisons (and paralysis) now wear off when the affliction level is low. +- Made medic AI react sooner to treat poisons. +- Attempts to poison the target can now fail when the user has a low medical skill. +- Added a short cooldown to applying medical items. Prevents being able to spam medicine (or poisons) at a nearly unlimited rate. +- Adjusted the syringe and stun gun dart trajectories. +- Reworked the weapons and the medical skill gains: instead of always increasing the weapons skill when a target takes damage, afflictions like poisons now increase the medical skill. Also applying the buffs now increases the medical skill. The skill increases are defined per affliction. +- Fixed the skill gains on low levels (0-15) being ridiculously high. + +Talents: +- Fixed "Down With the Ship" sometimes having an incorrect description. +- Fixed "Bounty Hunter" and "Logistics Expert" not giving 15% experience bonus. +- Pet raptor eggs can be fabricated from raptor eggs of any size. +- Fixed Flamer being craftable by anyone, even though it should only be possible with the "Pyromaniac" talent. +- Fixed ability to move in an unpowered exosuit when you've got any speed buff. +- Fixed "True Potential" and "Chonky Honks" not requiring clown power to work. +- Reduced the chance of finding genetic materials with the "Gene Harvester" talent, fixed genetic materials sometimes being found on defense bots. +- Fixed "Spec Ops" not working properly with shotguns and other weapons that fire multiple projectiles per shot. Only the first projectile that hit did double damage. +- Fixed some parts of Hemulen and Venture not flooding properly. +- "Mass production" talent only applies to items in the "material" category. Fixes e.g. recycle recipes sometimes allowing you to keep the original item. +- Fixed "Lone Wolf" not giving any damage/stun resistance. +- Fixed "Scrap Savant" having a 80% chance of finding scrap instead of 20%. +- Fixed "Steady Tune" not doing what the description says (giving a constantly diminishing 7.5% resistance instead of 60%), made the talent give 100% immunity instead. +- Fixed "Multifunctional" talent not giving a boost to crowbar damage. +- Fixed inaccurate "Unstoppable Curiosity" and "Ph.D in Nuclear Physics" descriptions. +- Fixed skill boost from "Journeyman" not matching the value in the description. +- Fixed "Quickdraw" damage bonus. +- Fixed inaccurate "Journeyman" description. +- Fixed "Oiled Machinery" not increasing fabrication speed. +- Fixed tinkered doors requiring a higher condition percentage to become repaired. +- Fixed "Drunken Sailor" not protecting entirely from the negative effects of drunkness. + +Multiplayer: +- Better support for playing the MP campaign without a host or someone with campaign management permissions on the server: + - You can vote to end the round in the campaign as well. + - Automatic restart works in the campaign mode as well. + - Anyone can manage salaries if there's no-one allowed to do it. + - If there's no host or anyone with permissions in the server, anyone is able to setup a new campaign. + - Campaign can be voted for when game mode voting is enabled. +- Made ragdoll syncing more robust: reduces cases of teleportation/desync when manually ragdolling the character in multiplayer. +- Fixed rejoining clients not regaining control of their character even if the character is still alive, if the client's IP address has changed. A respawn would also not trigger. +- Fixed "failed to write a network message, too much data is being stored in SegmentTable" errors that could occur in various situations: for example when the host has a large ban list, lots of submarines, and when rewiring a device with lots of connections. +- Fixed "Create new character" button not appearing in the tab menu when dead or spectating. +- Fixed clients not entering the server lobby if they accept a server invite during a single player round or tutorial. +- Fixed inability to join IPv4 servers when IPv6 is disabled. +- Fixed hidden submarine list sometimes desyncing if you have specific custom submarines. +- Fixed characters spawning with their inventory, skills, etc intact if they die during a round and the round ends when the client is no longer in the server. Or, to put it another way, spawning as if they hadn't died at all. +- Fixed an issue that prevented some dedicated servers from appearing in the server list. +- Fixed chat-linked wifi components not receiving order and report messages. +- Save the server settings after starting up the server to create the default settings file if it doesn't exist, instead of only when the server is shut down or the settings changed. +- Fixed list of hidden submarines sometimes desyncing. +- Fixed inability to start Linux dedicated server using LinuxGSM due to an incorrect EOL character in DedicatedServer.exe. + +Bugfixes: +- Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting. +- Fixed multiple issues in the tutorials: missing text, events not progressing when not following tutorial's steps, infographics usability issues. +- Fixed attacking with a melee weapon making you unable to turn (flip) for a while. +- Fixed ragdolling affecting the character's velocity, allowing it to be used as a way to avoid fall damage. +- Hull fixes to vanilla subs and wrecks. +- Fixed alien flares practically never spawning in ruins. +- Fixed status effects defined inside an attack definition still using the old OnUse/OnFailure logic instead of OnSuccess/OnFailure. +- Fixed "save as item assembly" and "snap to grid" buttons taking cursor focus in the sub editor even when they're not visible. +- Fixed inability to launch custom dedicated server executables from the main menu on mac and linux. +- Fixed inability to drag and drop items from the entity list to small containers (such as battery charging docks) in the sub editor. +- Fixed item condition not decreasing client-side if the condition decreases very slowly: for example when using a thorium rod with the "Cruisin'" talent. +- Fixed flares still emitting light after running out. +- Fixed Electrical Discharge Coil preview not working in the sub editor. +- Fixed alien flares not activating when clicking LMB. +- Fixed crawler's arms getting broken when the character flips (turns) in water. +- Fixed the recycle recipe of flak explosive ammo. +- Fixed misaligned shells in wrecked railgun shell rack. +- Fixed misaligned light component light sprite. +- Fixed crashing if the select audio device is disconnected while in the initial loading screen. +- Fixed inability to sell genetic materials that aren't 100% refined. +- Fixed liquid oxygenite exploding too easily. +- Fixed Hemulen's bottom airlock never fully draining. +- Fixed incorrect damage values in "Genetic Material (Hunter)" tooltip. +- Fixed exosuit not playing the warning beep when low or out of oxygen. +- Fixed "taste test" event showing for everyone and not progressing past the 1st prompt. +- Fixed "special training required" and "too many of this item" messages being shown to everyone when someone tries to place a portable pump. +- Added a pump to RemoraDrone's engine room, otherwise it's impossible to drain. +- Fixed some lighting sprites appearing dimmer than they should (most notably, junction boxes and supercapacitors). +- Fixed submarine's crush depth being displayed incorrectly on the campaign map when a submarine switch is pending: the hull upgrades of the current sub weren't taken into account, even though they carry over to the new sub. +- Fixed item assembly descriptions not showing up in the sub editor unless they're configured in a text file. +- Fixed rapid fissile accelerator ammo causing an explosion when launched, instead of when it hits something. +- Fixed bots sometimes trying to contain multiple ammo boxes when reloading turrets (also affected other items). +- Fixed "Manually Outfitted" not doing anything when starting a campaign. +- Fixed guardians trying to heal themselves in inactive pods (not destroyed pods, but ones deactivated via wiring). +- Fixed bots having trouble with fixing Barsuk's top hatch and leaks around it. +- Fixed sprite bleed in turret icons. +- Fixed radio channel presets resetting between rounds. +- Fixed some lines in the "shockjock" event being shown to everyone. +- Fixed radio noise playing even if you don't have a headset. +- Fixed hitscan projectiles going through subs when you're firing from inside another sub. +- Fixed lights set to flicker/pulse eventually getting out of sync even if they're set to the same frequency. +- Fixes to submarine preview: wires are now visible, scaled doors work correctly, camera is now correctly centered on the sub when opening the preview. +- Fixed the affliction type of some afflictions (like drunk) being "poison", causing them to be treated as poisons. They are now "debuffs" instead. +- Fixed defense bots not attacking pirates or bandits. Also fixed them not protecting the owner when attacked by the outpost guards. +- Fixed bots not knowing how to handle diving suits that don't have an oxygen tank inside them. Now they should be able to use them and refill them with an oxygen tank. + +Modding: +- Added a button for updating core mods into the Mods menu. +- Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases: + - Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to. + - Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon and Projectile components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons. + - MODDERS, PLEASE NOTE: Effects that target "Character" in the previously mentioned components now affect the user - if that's not the intention, the target should be changed to "UseTarget". + - The use target can now be a character, an item, or a structure, depending on the context. This allows effects that weren't previously possible, but due to it we'll now need to introduce some restrictions in the definitions in some cases. For example, we might want to use a conditional to check whether the target is of the right type, before triggering the status effect (). + - Added a new attribute for the MeleeWeapon component, "HitOnlyCharacters", which can be used for ignoring the hits to walls and items entirely. + - Due to the changes, some status effects that previously worked, might now need the "AllowWhenBroken" set to true in the definition to keep them working as they used to. So e.g. the "OnImpact" doesn't work anymore on your custom explosive, try that. +- Fixed crashing when trying to place a wreck with no hulls in a level. +- Fixed mod descriptions getting truncated to 255 characters when selecting an already-published item in the Mods menu. +- Fixed HMG's requiring hmgmagazine instead of any item with the type "hmgammo", making the use of modded ammo impossible without overriding the gun too. +- Fixed crashing when an upgrade tries to increase an integer value. +- Changed the affliction type of some afflictions, which might have implications on mods if they targeted them by type. +- Added AimAngle on Holdable item components. Can be used for defining a different hold angle for the aim and the rest poses. Note that on Holdables, the angles are mutually exclusive (defined separately), but on MeleeWeapons they are cumulative (added together). Therefore, no need to change the existing items! +- Fixed definitions not triggering the status effects that have the target type "UseTarget". + --------------------------------------------------------------------------------------------------------- v0.20.16.1 --------------------------------------------------------------------------------------------------------- diff --git a/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 f0bd29a02..76d2a689c 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -1,16 +1,73 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using Barotrauma; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Xunit; namespace TestProject; -public class INetSerializableStructImplementationChecks +public sealed class INetSerializableStructImplementationChecks { private delegate bool TryFindBehaviorDelegate(Type type, out NetSerializableProperties.IReadWriteBehavior behavior); + + private Type FillGenericParameters(Type type) + { + // Plug in some known good parameters to evaluate + // a concrete instance of this generic type + + var paramsConstraints = type.GetGenericArguments() + .Select(p => p.GetGenericParameterConstraints()) + .ToImmutableArray(); + + var chosenArgs = new Type[paramsConstraints.Length]; + + for (int i = 0; i < paramsConstraints.Length; i++) + { + var constraints = paramsConstraints[i]; + var baseTypeConstraints = constraints.Where(c => !c.IsGenericParameter); + + bool hasGenericConstraint(GenericParameterAttributes flag) + => constraints.Any(c + => c.IsGenericParameter && c.GenericParameterAttributes.HasFlag(flag)); + + bool refTypeConstraint = hasGenericConstraint(GenericParameterAttributes.ReferenceTypeConstraint); + bool valueTypeConstraint = baseTypeConstraints.Contains(typeof(ValueType)); + + if (refTypeConstraint && valueTypeConstraint) + { + throw new Exception($"Type \"{type.Name}\" has invalid generic constraints"); + } + + var viableArguments = new List(); + if (!refTypeConstraint) + { + // Value types are viable + viableArguments.AddRange(new[] + { + typeof(Vector2), + typeof(float), + typeof(int) + }); + } + if (!valueTypeConstraint) + { + // Reference types are viable + viableArguments.AddRange(new[] + { + typeof(string), + typeof(float[]), + typeof(int[]) + }); + } + + chosenArgs[i] = viableArguments.GetRandomUnsynced(); + } + return type.MakeGenericType(chosenArgs); + } [Fact] public void CheckStructMemberTypes() @@ -29,50 +86,10 @@ public class INetSerializableStructImplementationChecks foreach (var type in types) { - var concreteType = type; - if (type.IsGenericType) - { - // Plug in some known good parameters to evaluate - // a concrete instance of this generic type - - var paramsConstraints = type.GetGenericArguments() - .Select(p => p.GetGenericParameterConstraints()) - .ToImmutableArray(); + var concreteType = type.IsGenericType + ? FillGenericParameters(type) + : type; - var chosenArgs = new Type[paramsConstraints.Length]; - - for (int i = 0; i < paramsConstraints.Length; i++) - { - var constraints = paramsConstraints[i]; - bool refTypeConstraint = constraints.Any(c - => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); - bool valueTypeConstraint = constraints.Any(c - => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)); - if (refTypeConstraint && valueTypeConstraint) - { - throw new Exception($"Type \"{type.Name}\" has invalid generic constraints"); - } - - int rngMin = refTypeConstraint ? 3 : 0; - int rngMax = valueTypeConstraint ? 3 : 6; - - chosenArgs[i] = Rand.Range(rngMin, rngMax) switch - { - 0 => typeof(Vector2), - 1 => typeof(Point), - 2 => typeof(int), - - 3 => typeof(string), - 4 => typeof(float[]), - 5 => typeof(int[]), - - var invalid => throw new Exception($"Broken RNG ranges in test, got {invalid}") - }; - } - - concreteType = type.MakeGenericType(chosenArgs); - } - var members = NetSerializableProperties.GetPropertiesAndFields(concreteType); foreach (var member in members) { diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs index fdd87cc18..85b43d172 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -217,6 +217,9 @@ namespace TestProject public T NotSerializedFunction() => throw new NotImplementedException(); } + [NetworkSerialize] + private readonly record struct TestRecord(T Value) : INetSerializableStruct; + private struct TupleNullableStruct : INetSerializableStruct { [NetworkSerialize] @@ -248,24 +251,35 @@ namespace TestProject readStruct.IntValue.Should().Be(intValue); } - private static void SerializeDeserialize(T arg) where T : notnull + private static void SerializeDeserializeImpl(T toWrite) where T : INetSerializableStruct { ReadWriteMessage msg = new ReadWriteMessage(); - TestStruct writeStruct = new TestStruct - { - Value = arg - }; - msg.WriteNetSerializableStruct(writeStruct); + msg.WriteNetSerializableStruct(toWrite); msg.BitPosition = 0; - TestStruct readStruct = INetSerializableStruct.Read>(msg); + T read = INetSerializableStruct.Read(msg); - readStruct.Should().BeEquivalentTo(writeStruct, options => options - .ComparingByMembers>() + read.Should().BeEquivalentTo(toWrite, options => options + .ComparingByMembers() .ComparingByMembers(typeof(Option<>))); } + private static void SerializeDeserializeStruct(T arg) where T : notnull + => SerializeDeserializeImpl(new TestStruct + { + Value = arg + }); + + private static void SerializeDeserializeRecord(T arg) where T : notnull + => SerializeDeserializeImpl(new TestRecord(arg)); + + private static void SerializeDeserialize(T arg) where T : notnull + { + SerializeDeserializeStruct(arg); + SerializeDeserializeRecord(arg); + } + private static void SerializeDeserializeNullableTuple(T arg1, U arg2) { ReadWriteMessage msg = new ReadWriteMessage(); 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; } }; diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index d2765ade2..bddd50da5 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -124,7 +124,24 @@ namespace Lidgren.Network m_lastSocketBind = now; if (m_socket == null) - m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + { + try + { + m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + } + catch (SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.AddressFamilyNotSupported) + { + // Fall back to IPv4 if IPv6 is unsupported + m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + } + else + { + throw; + } + } + } if (reBind) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); @@ -132,9 +149,9 @@ namespace Lidgren.Network m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - m_socket.DualMode = m_configuration.UseDualModeSockets; + if (m_socket.AddressFamily == AddressFamily.InterNetworkV6) { m_socket.DualMode = m_configuration.UseDualModeSockets; } - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port); + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToFamily(m_socket.AddressFamily), reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); try @@ -413,6 +430,10 @@ namespace Lidgren.Network int bytesReceived = 0; try { + if (m_senderRemote is IPEndPoint ipEndpoint && ipEndpoint.AddressFamily != m_socket.AddressFamily) + { + m_senderRemote = ipEndpoint.MapToFamily(m_socket.AddressFamily); + } bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); } catch (SocketException sx) diff --git a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs index b49585afa..8a255010e 100644 --- a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -136,7 +136,7 @@ namespace Lidgren.Network { connectionReset = false; - target = NetUtility.MapToIPv6(target); + target = target.MapToFamily(m_socket.AddressFamily); IPAddress ba = default(IPAddress); try diff --git a/Libraries/Lidgren.Network/NetPeer.cs b/Libraries/Lidgren.Network/NetPeer.cs index eafef5d7c..538a87996 100644 --- a/Libraries/Lidgren.Network/NetPeer.cs +++ b/Libraries/Lidgren.Network/NetPeer.cs @@ -293,7 +293,7 @@ namespace Lidgren.Network if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); - remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); + remoteEndPoint = remoteEndPoint.MapToFamily(m_socket.AddressFamily); lock (m_connections) { diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index 0c437ce7f..cf5996e98 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -454,16 +454,24 @@ namespace Lidgren.Network return ComputeSHAHash(bytes, 0, bytes.Length); } - /// - /// Maps the IPEndPoint object to an IPv6 address, if it is currently mapped to an IPv4 address. - /// - internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) - { - if (endPoint.AddressFamily == AddressFamily.InterNetwork) - { - return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); - } - return endPoint; - } + internal static IPAddress MapToFamily(this IPAddress address, AddressFamily family) + { + switch (family) + { + case AddressFamily.InterNetworkV6: + return address.MapToIPv6(); + case AddressFamily.InterNetwork: + return address.MapToIPv4(); + default: + throw new Exception($"Unsupported address family: {family}"); + } + } + + internal static IPEndPoint MapToFamily(this IPEndPoint endpoint, AddressFamily family) + { + return endpoint.Address.AddressFamily == family + ? endpoint + : new IPEndPoint(endpoint.Address.MapToFamily(family), endpoint.Port); + } } } \ No newline at end of file diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index 441214b6e..2d08ff6ec 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -12,15 +12,22 @@ namespace Microsoft.Xna.Framework.Graphics { public interface ISpriteBatch { - public void Draw(Texture2D texture, - Vector2 position, - Rectangle? sourceRectangle, - Color color, - float rotation, - Vector2 origin, - Vector2 scale, - SpriteEffects effects, - float layerDepth); + public void Draw( + Texture2D texture, + Vector2 position, + Rectangle? sourceRectangle, + Color color, + float rotation, + Vector2 origin, + Vector2 scale, + SpriteEffects effects, + float layerDepth); + + public void Draw( + Texture2D texture, + VertexPositionColorTexture[] vertices, + float layerDepth, + int? count = null); } ///